commit a0333af35fb4829316616e51949ceee9f9263ea1
parent 4539d8571c98988bd48dcab6062366f9e8809750
Author: mdnrz <mehdeenoroozi@gmail.com>
Date: Tue, 15 Apr 2025 15:04:45 +0330
add simple tui client
Diffstat:
| M | README.md | | | 11 | +++++++++++ |
| A | client.go | | | 182 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | go.mod | | | 3 | --- |
| D | gotel.go | | | 190 | ------------------------------------------------------------------------------- |
| A | server.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)
+ }
+}