gotel

simple terminal chat program
git clone git://git.mdnr.space/gotel
Log | Files | Refs | README | LICENSE

commit a0333af35fb4829316616e51949ceee9f9263ea1
parent 4539d8571c98988bd48dcab6062366f9e8809750
Author: mdnrz <mehdeenoroozi@gmail.com>
Date:   Tue, 15 Apr 2025 15:04:45 +0330

add simple tui client

Diffstat:
MREADME.md | 11+++++++++++
Aclient.go | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dgo.mod | 3---
Dgotel.go | 190-------------------------------------------------------------------------------
Aserver.go | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 385 insertions(+), 193 deletions(-)

diff --git a/README.md b/README.md @@ -3,3 +3,14 @@ Simple telnet multi-user chat program. My attempt at learning golang. + +## Building and running + +`go build client.go` +`go build server.go` + +server: `./server` +client (remote or in another terminal locally): `./client` + +The rest is self-explanatory + diff --git a/client.go b/client.go @@ -0,0 +1,182 @@ +package main + +import ( + "fmt" + "net" + "log" + "strings" + + "github.com/jroimartin/gocui" +) + +type Command struct { + Name string + Description string + Signature string + Function func(*gocui.View, string) error +} + +const commandCnt = 3 +var commands [commandCnt]Command +var gui *gocui.Gui +var serverConn net.Conn + +const serverAddr = "127.0.0.1:6969" +const initMsg = +`This is a client for connecting to GoTel chat server. +You can use the following command to use this: +/help : Print the help menu +/login <UserName> : Login to the server +/quit : Logout from the server +=======================================================` + +func main() { + initCommands(); + gui, _ = gocui.NewGui(gocui.OutputNormal) + // if err != nil { + // log.Panicln(err) + // } + defer gui.Close() + + gui.Cursor = true; + gui.SetManagerFunc(layout) + + if err := gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + + if err := gui.SetKeybinding("prompt", gocui.KeyEnter, gocui.ModNone, getInput); err != nil { + log.Panicln(err) + } + + if err := gui.MainLoop(); err != nil && err != gocui.ErrQuit { + log.Panicln(err) + } +} + +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + chatLogW := maxX - 2 + chatLogH := maxY - 10 + promptX1 := 1 + promptY1 := chatLogH + 1 + promptX2 := maxX - 2 + promptY2 := maxY - 2 + if chatLog, err := g.SetView("chatLog", 1, 1, chatLogW, chatLogH); err != nil { + if err != gocui.ErrUnknownView { + return err + } + fmt.Fprintln(chatLog, initMsg) + } + if prompt, promptErr := g.SetView("prompt", promptX1, promptY1, promptX2, promptY2); promptErr != nil { + if promptErr != gocui.ErrUnknownView { + return promptErr + } + prompt.Editable = true; + prompt.Wrap = true; + if _,err := g.SetCurrentView("prompt"); err != nil { + return err; + } + } + return nil +} + +func getCommandArg(items []string) string { + if len(items) >= 2 { + return items[1] + } + return "" +} + +func getInput(g *gocui.Gui, v *gocui.View) error { + input := strings.TrimRight(v.Buffer(), "\r\n"); + items := strings.Split(input, " ") + v.Clear(); + v.SetCursor(0, 0); + chatLog, _ := g.View("chatLog"); + for _, cmd := range commands { + if strings.HasPrefix(cmd.Signature, items[0]) { + cmd.Function(chatLog, getCommandArg(items)); + return nil; + } + } + // fmt.Fprintf(chatLog, "you entered: %s\n", input); + serverConn.Write([]byte(input)) + return nil; +} + +func initCommands() { + commands = [commandCnt]Command { + + Command { + Name: "help", + Description: "Print help menu", + Signature: "/help", + Function: printHelp, + }, + + Command { + Name: "login", + Description: "Login to server", + Signature: "/login <UserNmae>", + Function: sendLogin, + }, + + Command { + Name: "quit", + Description: "Logout from server", + Signature: "/quit", + Function: sendQuit, + }, + } +} + +func printHelp(v *gocui.View, input string) error { + for _, cmd := range commands { + fmt.Fprintf(v, "%s - %s\n", cmd.Signature, cmd.Description) + } + return nil +} + +func sendLogin(v *gocui.View, input string) error { + serverConn, _ = net.Dial("tcp", serverAddr); + // if err != nil { + // return err + // } + _, err := serverConn.Write([]byte(input)); + if err != nil { + return err + } + go getMsg(serverConn) + return nil +} + +func sendQuit (v *gocui.View, input string) error { + fmt.Fprintln(v, "Quiting from server"); + serverConn.Close() + return nil +} + +func getMsg (conn net.Conn) { + readBuf := make([]byte, 512) + for { + n, readErr := conn.Read(readBuf); + + if readErr != nil { + return + } + gui.Update(func(g *gocui.Gui) error { + v, err := gui.View("chatLog") + if err != nil { + return err + } + fmt.Fprintln(v, string(readBuf[:n])) + return nil + }) + } +} + + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} diff --git a/go.mod b/go.mod @@ -1,3 +0,0 @@ -module gotel - -go 1.24.0 diff --git a/gotel.go b/gotel.go @@ -1,190 +0,0 @@ -package main - -import ( - "log" - "net" - "time" - "strings" - "fmt" -) - -type msgType int - -const ( - msgConnect msgType = iota + 1 - msgLogin - msgText - msgQuit -) - -type Client struct { - UserName string - LastMsgTime time.Time - Request msgType - Strike int - Banned bool - BanEnd time.Time - Conn net.Conn - Text string -} - -const ( - msgCoolDownTimeSec = 1 - banLimit = 5 - banTimeoutSec = 180 - Port = "6969" -) - -func addClient(conn net.Conn, Client_q chan Client) { - loginPrompt := "Who are you?\n> " - _, err := conn.Write([]byte(loginPrompt)) - if err != nil { - log.Printf("[ERROR] Could not send login prompt to user %s: %s\n", - conn.RemoteAddr().String(), err) - } - readBuf := make([]byte, 512) - for { - n, err := conn.Read(readBuf) - if n == 0 { - Client_q <- Client { - Request: msgQuit, - Conn: conn, - } - return; - } - if n > 0 { - Client_q <- Client { - Request: msgText, - Conn: conn, - Text: string(readBuf[:n]), - } - } - if err != nil { - log.Printf("Could not read message from client %s: %s\n", conn.RemoteAddr().String(), err) - conn.Close() - return - } - } -} - -func canMessage(client *Client) bool { - if !client.Banned { - diff := time.Now().Sub(client.LastMsgTime).Seconds(); - if diff <= msgCoolDownTimeSec { - client.Strike += 1; - if client.Strike >= banLimit { - client.Banned = true; - client.BanEnd = time.Now().Add(banTimeoutSec * time.Second) - } - return false; - } - return true - } - banTimeRemaining := client.BanEnd.Sub(time.Now()).Seconds(); - if banTimeRemaining >= 0.0 { - banTimeRemainingStr := fmt.Sprintf("You're banned. Try again in %.0f seconds.\n", banTimeRemaining) - client.Conn.Write([]byte(banTimeRemainingStr)); - return false; - } - client.Strike = 0; - client.Banned = false; - return true; -} - -func checkForDuplicateUN(needle string, heystack map[string]Client) bool { - for _, client := range heystack { - if client.UserName == needle { return true } - } - return false; -} - -func server(Client_q chan Client) { - clientsOnline := make(map[string]Client) - clientsOffline := make(map[string]Client) - for { - client := <-Client_q - keyString := client.Conn.RemoteAddr().String(); - switch client.Request { - case msgConnect: - // TODO: implement rate limit for connection requests - log.Printf("Got login request from %s\n", keyString); - clientsOffline[keyString] = client; - case msgQuit: - author, ok := clientsOnline[keyString] - if ok { - log.Printf("%s logged out.\n", author.UserName); - author.Conn.Close(); - delete(clientsOnline, keyString); - } - case msgText: - clientOffline, ok := clientsOffline[keyString]; - if ok { - if !canMessage(&clientOffline) { - clientsOffline[keyString] = clientOffline - break; - } - clientOffline.UserName = strings.TrimRight(client.Text, "\r\n"); - clientOffline.LastMsgTime = time.Now(); - clientsOffline[keyString] = clientOffline - if checkForDuplicateUN(clientOffline.UserName, clientsOnline) { - _, err := clientsOffline[keyString].Conn.Write([]byte("UserName already exists; Try something else\n> ")); - if err != nil { - log.Printf("Could not send message to client %s\n", keyString) - } - break; - } - log.Printf("logging in %s\n", clientOffline.UserName); - clientsOnline[keyString] = clientOffline; - delete(clientsOffline, keyString); - _, err := clientsOnline[keyString].Conn.Write([]byte("Welcome " + clientsOnline[keyString].UserName + "\n\n")); - if err != nil { - log.Printf("Could not send message to client %s\n", clientsOnline[keyString].UserName) - } - break; - } - - author, ok := clientsOnline[keyString]; - if !ok { - log.Fatal("cannot find client\n"); - } - if !canMessage(&author) { - clientsOnline[keyString] = author - break; - } - author.LastMsgTime = time.Now() - author.Text = client.Text; - clientsOnline[keyString] = author - for _, value := range clientsOnline { - if value.Conn == author.Conn { - continue - } - _, err := value.Conn.Write([]byte(author.UserName + ": " + author.Text)) - if err != nil { - log.Printf("Could not send message to client %s\n", value.UserName) - } - } - } - } -} - -func main() { - ln, err := net.Listen("tcp", ":"+Port) - if err != nil { - log.Fatalf("Could not listen to port %s: %s\n", Port, err) - } - Client_q := make(chan Client) - go server(Client_q) - for { - conn, err := ln.Accept() - if err != nil { - log.Printf("Could not accept the connection: %s\n", err) - continue - } - log.Printf("Accepted connection from %s", conn.RemoteAddr()) - Client_q <- Client { - Request: msgConnect, - Conn: conn, - } - go addClient(conn, Client_q) - } -} diff --git a/server.go b/server.go @@ -0,0 +1,192 @@ +package main + +import ( + "log" + "net" + "time" + "strings" + "fmt" +) + +var commands [3]string = [3]string{"/help", "/login", "/quit"}; + +type msgType int + +const ( + msgConnect msgType = iota + 1 + msgLogin + msgText + msgQuit +) + +type Client struct { + UserName string + LastMsgTime time.Time + Request msgType + Strike int + Banned bool + BanEnd time.Time + Conn net.Conn + Text string +} + +const ( + msgCoolDownTimeSec = 1 + banLimit = 5 + banTimeoutSec = 180 + Port = "6969" +) + +func addClient(conn net.Conn, Client_q chan Client) { + // loginPrompt := "Who are you?\n> " + // _, err := conn.Write([]byte(loginPrompt)) + // if err != nil { + // log.Printf("[ERROR] Could not send login prompt to user %s: %s\n", + // conn.RemoteAddr().String(), err) + // } + readBuf := make([]byte, 512) + for { + n, err := conn.Read(readBuf) + if n == 0 { + Client_q <- Client { + Request: msgQuit, + Conn: conn, + } + return; + } + if n > 0 { + Client_q <- Client { + Request: msgText, + Conn: conn, + Text: string(readBuf[:n]), + } + } + if err != nil { + log.Printf("Could not read message from client %s: %s\n", conn.RemoteAddr().String(), err) + conn.Close() + return + } + } +} + +func canMessage(client *Client) bool { + if !client.Banned { + diff := time.Now().Sub(client.LastMsgTime).Seconds(); + if diff <= msgCoolDownTimeSec { + client.Strike += 1; + if client.Strike >= banLimit { + client.Banned = true; + client.BanEnd = time.Now().Add(banTimeoutSec * time.Second) + } + return false; + } + return true + } + banTimeRemaining := client.BanEnd.Sub(time.Now()).Seconds(); + if banTimeRemaining >= 0.0 { + banTimeRemainingStr := fmt.Sprintf("You're banned. Try again in %.0f seconds.\n", banTimeRemaining) + client.Conn.Write([]byte(banTimeRemainingStr)); + return false; + } + client.Strike = 0; + client.Banned = false; + return true; +} + +func checkForDuplicateUN(needle string, heystack map[string]Client) bool { + for _, client := range heystack { + if client.UserName == needle { return true } + } + return false; +} + +func server(Client_q chan Client) { + clientsOnline := make(map[string]Client) + clientsOffline := make(map[string]Client) + for { + client := <-Client_q + keyString := client.Conn.RemoteAddr().String(); + switch client.Request { + case msgConnect: + // TODO: implement rate limit for connection requests + log.Printf("Got login request from %s\n", keyString); + clientsOffline[keyString] = client; + case msgQuit: + author, ok := clientsOnline[keyString] + if ok { + log.Printf("%s logged out.\n", author.UserName); + author.Conn.Close(); + delete(clientsOnline, keyString); + } + case msgText: + clientOffline, ok := clientsOffline[keyString]; + if ok { + if !canMessage(&clientOffline) { + clientsOffline[keyString] = clientOffline + break; + } + clientOffline.UserName = strings.TrimRight(client.Text, "\r\n"); + clientOffline.LastMsgTime = time.Now(); + clientsOffline[keyString] = clientOffline + if checkForDuplicateUN(clientOffline.UserName, clientsOnline) { + _, err := clientsOffline[keyString].Conn.Write([]byte("UserName already exists; Try something else.")); + if err != nil { + log.Printf("Could not send message to client %s\n", keyString) + } + break; + } + log.Printf("logging in %s\n", clientOffline.UserName); + clientsOnline[keyString] = clientOffline; + delete(clientsOffline, keyString); + _, err := clientsOnline[keyString].Conn.Write([]byte("Welcome " + clientsOnline[keyString].UserName + "\n")); + if err != nil { + log.Printf("Could not send message to client %s\n", clientsOnline[keyString].UserName) + } + break; + } + + author, ok := clientsOnline[keyString]; + if !ok { + log.Fatal("cannot find client\n"); + } + if !canMessage(&author) { + clientsOnline[keyString] = author + break; + } + author.LastMsgTime = time.Now() + author.Text = client.Text; + clientsOnline[keyString] = author + for _, value := range clientsOnline { + // if value.Conn == author.Conn { + // continue + // } + _, err := value.Conn.Write([]byte(author.UserName + ": " + author.Text)) + if err != nil { + log.Printf("Could not send message to client %s\n", value.UserName) + } + } + } + } +} + +func main() { + ln, err := net.Listen("tcp", ":"+Port) + if err != nil { + log.Fatalf("Could not listen to port %s: %s\n", Port, err) + } + Client_q := make(chan Client) + go server(Client_q) + for { + conn, err := ln.Accept() + if err != nil { + log.Printf("Could not accept the connection: %s\n", err) + continue + } + log.Printf("Accepted connection from %s", conn.RemoteAddr()) + Client_q <- Client { + Request: msgConnect, + Conn: conn, + } + go addClient(conn, Client_q) + } +}