From 4c945ffaaefc098722a91e9086835b7f7c03a19e Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Fri, 21 Oct 2022 13:47:44 +0530 Subject: [PATCH 1/6] refactor: TUI --- tui/{main.go => tui.go} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tui/{main.go => tui.go} (99%) diff --git a/tui/main.go b/tui/tui.go similarity index 99% rename from tui/main.go rename to tui/tui.go index 1f0d0ab..5a8e62b 100644 --- a/tui/main.go +++ b/tui/tui.go @@ -9,7 +9,7 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -func main() { +func UI() { p := tea.NewProgram(initialModel()) if err := p.Start(); err != nil { log.Fatal(err) From 58288444b8c99695c6b934282b0727de735442a6 Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Fri, 21 Oct 2022 13:56:32 +0530 Subject: [PATCH 2/6] feat: add client and server --- client/main.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ server/main.go | 71 ++++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 client/main.go create mode 100644 server/main.go diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..52be0b4 --- /dev/null +++ b/client/main.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "net/url" + "os" + "os/signal" + "time" + + "github.com/gorilla/websocket" +) + +// generateNewState generates a random character that is appended to the client state. Used for testing purposes. +func generateNewState() string { + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + + return string(letters[rand.Intn(len(letters))]) +} + +func main() { + // Server address. + addr := "localhost:8080" + + // Set up interrupt channel. + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + // Construct WebSocket URL. + u := url.URL{Scheme: "ws", Host: addr, Path: "/echo"} + log.Printf("connecting to %s", u.String()) + + // Connect to WebSocket. + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + log.Fatal("dial:", err) + } + defer c.Close() + + // Set up done channel. + done := make(chan struct{}) + + // Read username from the user. + username := "invalid" + fmt.Print("username: ") + fmt.Scanf("%s", &username) + + // Initialize user state. + state := username + " " + + go func() { + defer close(done) + for { + // Read messages from the connection. + _, message, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + return + } + log.Printf("updated state: %s", message) + } + }() + + // Set up a ticker. + // Interval is set to 4 seconds. + ticker := time.NewTicker(4 * time.Second) + defer ticker.Stop() + + for { + select { + case <-done: + return + case <-ticker.C: + // On ticker event, update state. + state += generateNewState() + // Send message to server. + err := c.WriteMessage(websocket.TextMessage, []byte(state)) + if err != nil { + log.Println("write:", err) + return + } + case <-interrupt: + log.Println("interrupt") + // Cleanly close the connection by sending a close message. + err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + log.Println("write close:", err) + return + } + select { + case <-done: + case <-time.After(time.Second): + } + return + } + } +} diff --git a/go.mod b/go.mod index e8b0379..ded2961 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/charmbracelet/bubbles v0.14.0 github.com/charmbracelet/bubbletea v0.22.1 + github.com/gorilla/websocket v1.5.0 ) require ( diff --git a/go.sum b/go.sum index ddbf585..06e1914 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DA github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..f6be888 --- /dev/null +++ b/server/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/gorilla/websocket" +) + +// Server address. +var addr = "localhost:8080" + +// Temp counter. +var counter = 0 + +// Connection upgrader. +var upgrader = websocket.Upgrader{} + +// Connection list. +var conns []*websocket.Conn + +// Echo handler. +func echo(w http.ResponseWriter, r *http.Request) { + // Upgrade to WebSocket connection. + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Print("upgrade:", err) + return + } + defer c.Close() + + // Add connection to connection list. + conns = append(conns, c) + + for { + // Broadcast + for i, c := range conns { + mt, message, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + + // Log for debugging purposes. + log.Printf("reading from conn %d, got state from: %s", i, message) + + // Increment counter. + counter += 1 + + // Construct a new message to send to the client. + newMessage := fmt.Sprintf("conns: %d, %d %s", len(conns), counter, string(message)) + + // Send message to client. + err = c.WriteMessage(mt, []byte(newMessage)) + if err != nil { + log.Println("write:", err) + break + } + } + } +} + +func main() { + // Register echo handler. + http.HandleFunc("/echo", echo) + + // Start server. + log.Printf("starting server on %s", addr) + log.Fatal(http.ListenAndServe(addr, nil)) +} From 810f66a59effc9cb5baad71e4ee14b51c049da99 Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Fri, 21 Oct 2022 14:12:49 +0530 Subject: [PATCH 3/6] feat: add example --- broadcast/client.go | 133 ++++++++++++++++++++++++++++++++++++++++++++ broadcast/home.html | 98 ++++++++++++++++++++++++++++++++ broadcast/hub.go | 53 ++++++++++++++++++ broadcast/main.go | 36 ++++++++++++ 4 files changed, 320 insertions(+) create mode 100644 broadcast/client.go create mode 100644 broadcast/home.html create mode 100644 broadcast/hub.go create mode 100644 broadcast/main.go diff --git a/broadcast/client.go b/broadcast/client.go new file mode 100644 index 0000000..710abde --- /dev/null +++ b/broadcast/client.go @@ -0,0 +1,133 @@ +package main + +import ( + "bytes" + "log" + "net/http" + "time" + + "github.com/gorilla/websocket" +) + +const ( + // Time allowed to write a message to the peer. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Maximum message size allowed from peer. + maxMessageSize = 512 +) + +var ( + newline = []byte{'\n'} + space = []byte{' '} +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +// Client is a middleman between the websocket connection and the hub. +type Client struct { + hub *Hub + + // The websocket connection. + conn *websocket.Conn + + // Buffered channel of outbound messages. + send chan []byte +} + +// readPump pumps messages from the websocket connection to the hub. +// +// The application runs readPump in a per-connection goroutine. The application +// ensures that there is at most one reader on a connection by executing all +// reads from this goroutine. +func (c *Client) readPump() { + defer func() { + c.hub.Unregister <- c + c.conn.Close() + }() + c.conn.SetReadLimit(maxMessageSize) + c.conn.SetReadDeadline(time.Now().Add(pongWait)) + c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + for { + _, message, err := c.conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("error: %v", err) + } + break + } + message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) + c.hub.Broadcast <- message + } +} + +// writePump pumps messages from the hub to the websocket connection. +// +// A goroutine running writePump is started for each connection. The +// application ensures that there is at most one writer to a connection by +// executing all writes from this goroutine. +func (c *Client) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.conn.Close() + }() + for { + select { + case message, ok := <-c.send: + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if !ok { + // The hub closed the channel. + c.conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + w, err := c.conn.NextWriter(websocket.TextMessage) + if err != nil { + return + } + w.Write(message) + + // Add queued chat messages to the current websocket message. + n := len(c.send) + for i := 0; i < n; i++ { + w.Write(newline) + w.Write(<-c.send) + } + + if err := w.Close(); err != nil { + return + } + case <-ticker.C: + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +// serveWs handles websocket requests from the peer. +func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)} + client.hub.Register <- client + + // Allow collection of memory referenced by the caller by doing all work in + // new goroutines. + go client.writePump() + go client.readPump() +} diff --git a/broadcast/home.html b/broadcast/home.html new file mode 100644 index 0000000..bf866af --- /dev/null +++ b/broadcast/home.html @@ -0,0 +1,98 @@ + + + +Chat Example + + + + +
+
+ + +
+ + diff --git a/broadcast/hub.go b/broadcast/hub.go new file mode 100644 index 0000000..0b71799 --- /dev/null +++ b/broadcast/hub.go @@ -0,0 +1,53 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +// Hub maintains the set of active clients and broadcasts messages to the +// clients. +type Hub struct { + // Registered clients. + Clients map[*Client]bool + + // Inbound messages from the clients. + Broadcast chan []byte + + // Register requests from the clients. + Register chan *Client + + // Unregister requests from clients. + Unregister chan *Client +} + +func newHub() *Hub { + return &Hub{ + Broadcast: make(chan []byte), + Register: make(chan *Client), + Unregister: make(chan *Client), + Clients: make(map[*Client]bool), + } +} + +func (h *Hub) run() { + for { + select { + case client := <-h.Register: + h.Clients[client] = true + case client := <-h.Unregister: + if _, ok := h.Clients[client]; ok { + delete(h.Clients, client) + close(client.send) + } + case message := <-h.Broadcast: + for client := range h.Clients { + select { + case client.send <- message: + default: + close(client.send) + delete(h.Clients, client) + } + } + } + } +} diff --git a/broadcast/main.go b/broadcast/main.go new file mode 100644 index 0000000..a41994c --- /dev/null +++ b/broadcast/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "log" + "net/http" +) + +var addr = flag.String("addr", ":8080", "http service address") + +func serveHome(w http.ResponseWriter, r *http.Request) { + log.Println(r.URL) + if r.URL.Path != "/" { + http.Error(w, "Not found", http.StatusNotFound) + return + } + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + http.ServeFile(w, r, "home.html") +} + +func main() { + flag.Parse() + hub := newHub() + go hub.run() + http.HandleFunc("/", serveHome) + http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { + ServeWs(hub, w, r) + }) + err := http.ListenAndServe(*addr, nil) + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +} From de19d3a99190f263fe1b6a039eabef66fc012729 Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Sat, 22 Oct 2022 19:05:53 +0530 Subject: [PATCH 4/6] refactor: set up client and server --- broadcast/client.go | 133 -------------------------------------- broadcast/home.html | 98 ---------------------------- broadcast/hub.go | 53 --------------- broadcast/main.go | 36 ----------- client/main.go | 152 ++++++++++++++++++++++++++------------------ go.mod | 3 + go.sum | 9 +++ server/main.go | 127 +++++++++++++++++++++++------------- 8 files changed, 183 insertions(+), 428 deletions(-) delete mode 100644 broadcast/client.go delete mode 100644 broadcast/home.html delete mode 100644 broadcast/hub.go delete mode 100644 broadcast/main.go diff --git a/broadcast/client.go b/broadcast/client.go deleted file mode 100644 index 710abde..0000000 --- a/broadcast/client.go +++ /dev/null @@ -1,133 +0,0 @@ -package main - -import ( - "bytes" - "log" - "net/http" - "time" - - "github.com/gorilla/websocket" -) - -const ( - // Time allowed to write a message to the peer. - writeWait = 10 * time.Second - - // Time allowed to read the next pong message from the peer. - pongWait = 60 * time.Second - - // Send pings to peer with this period. Must be less than pongWait. - pingPeriod = (pongWait * 9) / 10 - - // Maximum message size allowed from peer. - maxMessageSize = 512 -) - -var ( - newline = []byte{'\n'} - space = []byte{' '} -) - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - -// Client is a middleman between the websocket connection and the hub. -type Client struct { - hub *Hub - - // The websocket connection. - conn *websocket.Conn - - // Buffered channel of outbound messages. - send chan []byte -} - -// readPump pumps messages from the websocket connection to the hub. -// -// The application runs readPump in a per-connection goroutine. The application -// ensures that there is at most one reader on a connection by executing all -// reads from this goroutine. -func (c *Client) readPump() { - defer func() { - c.hub.Unregister <- c - c.conn.Close() - }() - c.conn.SetReadLimit(maxMessageSize) - c.conn.SetReadDeadline(time.Now().Add(pongWait)) - c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) - for { - _, message, err := c.conn.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - log.Printf("error: %v", err) - } - break - } - message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) - c.hub.Broadcast <- message - } -} - -// writePump pumps messages from the hub to the websocket connection. -// -// A goroutine running writePump is started for each connection. The -// application ensures that there is at most one writer to a connection by -// executing all writes from this goroutine. -func (c *Client) writePump() { - ticker := time.NewTicker(pingPeriod) - defer func() { - ticker.Stop() - c.conn.Close() - }() - for { - select { - case message, ok := <-c.send: - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if !ok { - // The hub closed the channel. - c.conn.WriteMessage(websocket.CloseMessage, []byte{}) - return - } - - w, err := c.conn.NextWriter(websocket.TextMessage) - if err != nil { - return - } - w.Write(message) - - // Add queued chat messages to the current websocket message. - n := len(c.send) - for i := 0; i < n; i++ { - w.Write(newline) - w.Write(<-c.send) - } - - if err := w.Close(); err != nil { - return - } - case <-ticker.C: - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { - return - } - } - } -} - -// serveWs handles websocket requests from the peer. -func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Println(err) - return - } - client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)} - client.hub.Register <- client - - // Allow collection of memory referenced by the caller by doing all work in - // new goroutines. - go client.writePump() - go client.readPump() -} diff --git a/broadcast/home.html b/broadcast/home.html deleted file mode 100644 index bf866af..0000000 --- a/broadcast/home.html +++ /dev/null @@ -1,98 +0,0 @@ - - - -Chat Example - - - - -
-
- - -
- - diff --git a/broadcast/hub.go b/broadcast/hub.go deleted file mode 100644 index 0b71799..0000000 --- a/broadcast/hub.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package main - -// Hub maintains the set of active clients and broadcasts messages to the -// clients. -type Hub struct { - // Registered clients. - Clients map[*Client]bool - - // Inbound messages from the clients. - Broadcast chan []byte - - // Register requests from the clients. - Register chan *Client - - // Unregister requests from clients. - Unregister chan *Client -} - -func newHub() *Hub { - return &Hub{ - Broadcast: make(chan []byte), - Register: make(chan *Client), - Unregister: make(chan *Client), - Clients: make(map[*Client]bool), - } -} - -func (h *Hub) run() { - for { - select { - case client := <-h.Register: - h.Clients[client] = true - case client := <-h.Unregister: - if _, ok := h.Clients[client]; ok { - delete(h.Clients, client) - close(client.send) - } - case message := <-h.Broadcast: - for client := range h.Clients { - select { - case client.send <- message: - default: - close(client.send) - delete(h.Clients, client) - } - } - } - } -} diff --git a/broadcast/main.go b/broadcast/main.go deleted file mode 100644 index a41994c..0000000 --- a/broadcast/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "flag" - "log" - "net/http" -) - -var addr = flag.String("addr", ":8080", "http service address") - -func serveHome(w http.ResponseWriter, r *http.Request) { - log.Println(r.URL) - if r.URL.Path != "/" { - http.Error(w, "Not found", http.StatusNotFound) - return - } - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - http.ServeFile(w, r, "home.html") -} - -func main() { - flag.Parse() - hub := newHub() - go hub.run() - http.HandleFunc("/", serveHome) - http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { - ServeWs(hub, w, r) - }) - err := http.ListenAndServe(*addr, nil) - if err != nil { - log.Fatal("ListenAndServe: ", err) - } -} diff --git a/client/main.go b/client/main.go index 52be0b4..bc22688 100644 --- a/client/main.go +++ b/client/main.go @@ -1,98 +1,124 @@ package main import ( + "bufio" + "flag" "fmt" "log" - "math/rand" "net/url" "os" - "os/signal" - "time" + "github.com/fatih/color" "github.com/gorilla/websocket" ) -// generateNewState generates a random character that is appended to the client state. Used for testing purposes. -func generateNewState() string { - var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") +type ConnReader interface { + ReadJSON(v interface{}) error +} + +type ConnWriter interface { + WriteJSON(v interface{}) error + Close() error +} + +type Scanner interface { + Scan() bool + Text() string +} - return string(letters[rand.Intn(len(letters))]) +type message struct { + Username string `json:"username"` + Text string `json:"text"` + Type string `json:"type"` } func main() { - // Server address. - addr := "localhost:8080" + var name string + var s *bufio.Scanner - // Set up interrupt channel. - interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, os.Interrupt) + // Parse flags. + server := flag.String("server", "localhost:9000", "Server network address") + path := flag.String("path", "/", "Server path") + flag.Parse() // Construct WebSocket URL. - u := url.URL{Scheme: "ws", Host: addr, Path: "/echo"} - log.Printf("connecting to %s", u.String()) + u := url.URL{Scheme: "ws", Host: *server, Path: *path} - // Connect to WebSocket. - c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + // Read username. + fmt.Printf("%s", color.YellowString("Enter your Name: ")) + s = bufio.NewScanner(os.Stdin) + s.Scan() + name = s.Text() + + // Display welcome message. + color.Green("\nWelcome %s!\n", name) + color.Green("Connecting to server @ %s\n", *server) + color.Yellow("Send a message, or type !q to exit.\n") + + // Get WebSocket connection. + conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { - log.Fatal("dial:", err) + color.Red("Connection error, exiting: %s", err) + os.Exit(0) } - defer c.Close() + defer conn.Close() - // Set up done channel. - done := make(chan struct{}) + // Send joining message. + msg := message{Username: name, Text: "has joined the chat.", Type: "info"} + conn.WriteJSON(msg) - // Read username from the user. - username := "invalid" - fmt.Print("username: ") - fmt.Scanf("%s", &username) + go readMessages(conn) // Handle incoming messages concurrently. + writeMessages(conn, s, name) // Handle outgoing messages concurrently. +} - // Initialize user state. - state := username + " " +// readMessages handles incoming messages on the WebSocket connection. +func readMessages(conn ConnReader) { + for { + var msg message - go func() { - defer close(done) - for { - // Read messages from the connection. - _, message, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - return - } - log.Printf("updated state: %s", message) + // Read message. + err := conn.ReadJSON(&msg) + if err != nil { + color.Red("Server closed. Exiting...") + os.Exit(0) } - }() - // Set up a ticker. - // Interval is set to 4 seconds. - ticker := time.NewTicker(4 * time.Second) - defer ticker.Stop() + // Display message. + color.Magenta("%s: %s\n", msg.Username, msg.Text) + } +} + +// writeMessages scans stdin and sends each scanned line to the server as JSON. +func writeMessages(conn ConnWriter, s Scanner, name string) { + var msg message + msg.Username = name for { - select { - case <-done: - return - case <-ticker.C: - // On ticker event, update state. - state += generateNewState() - // Send message to server. - err := c.WriteMessage(websocket.TextMessage, []byte(state)) - if err != nil { - log.Println("write:", err) - return + fmt.Print("> ") + if s.Scan() { + fmt.Printf("\033[A") + msg.Text = s.Text() + + // Handle quit event. + if msg.Text == "!q" { + fmt.Println("Goodbye!") + conn.WriteJSON(message{Username: name, Text: "has disconnected.", Type: "info"}) + conn.Close() + os.Exit(0) } - case <-interrupt: - log.Println("interrupt") - // Cleanly close the connection by sending a close message. - err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - if err != nil { - log.Println("write close:", err) - return + + // Display message. + if msg.Type != "" { + color.Cyan("%s %s\n", msg.Username, msg.Text) + } else { + color.Cyan("%s: %s\n", msg.Username, msg.Text) } - select { - case <-done: - case <-time.After(time.Second): + + // Write message to connection. + err := conn.WriteJSON(msg) + if err != nil { + log.Fatal("Error sending message, exiting") } - return } } } diff --git a/go.mod b/go.mod index ded2961..94d45ae 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.18 require ( github.com/charmbracelet/bubbles v0.14.0 github.com/charmbracelet/bubbletea v0.22.1 + github.com/fatih/color v1.13.0 + github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 ) @@ -13,6 +15,7 @@ require ( github.com/charmbracelet/lipgloss v0.5.0 // indirect github.com/containerd/console v1.0.3 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect diff --git a/go.sum b/go.sum index 06e1914..1c5ce4c 100644 --- a/go.sum +++ b/go.sum @@ -10,11 +10,18 @@ github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DA github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -39,6 +46,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/server/main.go b/server/main.go index f6be888..aabc332 100644 --- a/server/main.go +++ b/server/main.go @@ -1,71 +1,108 @@ package main import ( - "fmt" + "flag" "log" "net/http" + "time" + "github.com/fatih/color" + "github.com/google/uuid" "github.com/gorilla/websocket" ) -// Server address. -var addr = "localhost:8080" - -// Temp counter. -var counter = 0 +type message struct { + Username string `json:"username"` + Text string `json:"text"` + Type string `json:"type"` + ID uuid.UUID +} -// Connection upgrader. +// Upgrader instance to upgrade all HTTP connections to a WebSocket. var upgrader = websocket.Upgrader{} -// Connection list. -var conns []*websocket.Conn +// Map to store currently active client connections. +var activeClients = make(map[*websocket.Conn]uuid.UUID) + +// Channel for client messages. +var messageChan = make(chan message) + +func main() { + // Parse flags. + addr := flag.String("addr", ":9000", "Server's network address") + flag.Parse() + + mux := http.NewServeMux() + mux.HandleFunc("/", handleConn) -// Echo handler. -func echo(w http.ResponseWriter, r *http.Request) { - // Upgrade to WebSocket connection. - c, err := upgrader.Upgrade(w, r, nil) + // Handle incoming messages. + go handleMsg() + + // Start the server. + log.Printf("Starting server on %s", *addr) + err := http.ListenAndServe(*addr, mux) if err != nil { - log.Print("upgrade:", err) - return + log.Fatal("Error starting server, exiting.", err) } - defer c.Close() +} - // Add connection to connection list. - conns = append(conns, c) +// handleConn handles incoming HTTP connections by adding the connection to activeClients and reads messages from the connection. +func handleConn(w http.ResponseWriter, r *http.Request) { + // Upgrade incoming HTTP connections to WebSocket connections + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Error upgrading connection to websocket: %v", err) + } + defer conn.Close() - for { - // Broadcast - for i, c := range conns { - mt, message, err := c.ReadMessage() - if err != nil { - log.Println("read:", err) - break - } + // Generate a UUID for the client. + activeClients[conn] = uuid.New() - // Log for debugging purposes. - log.Printf("reading from conn %d, got state from: %s", i, message) + for { + var msg message - // Increment counter. - counter += 1 + // Read message from the connection. + err := conn.ReadJSON(&msg) + if err != nil { + log.Printf("Closing connection with ID: %v", activeClients[conn]) + delete(activeClients, conn) + break + } - // Construct a new message to send to the client. - newMessage := fmt.Sprintf("conns: %d, %d %s", len(conns), counter, string(message)) + // Set message ID + msg.ID = activeClients[conn] - // Send message to client. - err = c.WriteMessage(mt, []byte(newMessage)) - if err != nil { - log.Println("write:", err) - break - } - } + // Send message to messageChan. + messageChan <- msg } } -func main() { - // Register echo handler. - http.HandleFunc("/echo", echo) +// handleMsg listens to the messageChan channel and broadcasts messages to other clients. +func handleMsg() { + for { + // Get message from messageChan. + msg := <-messageChan - // Start server. - log.Printf("starting server on %s", addr) - log.Fatal(http.ListenAndServe(addr, nil)) + // Log each message to stdout. + t := time.Now().Format(time.ANSIC) + if msg.Type != "" { + color.Green("%s >> %s %s\n", t, msg.Username, msg.Text) + } else { + color.Green("%s >> %s: %s\n", t, msg.Username, msg.Text) + } + + // Broadcast to all active clients. + for client, UUID := range activeClients { + // Check the UUID to prevent sending messages to their origin. + if msg.ID != UUID { + // Write JSON message. + err := client.WriteJSON(msg) + if err != nil { + log.Printf("Error sending message to client: %v", err) + client.Close() + delete(activeClients, client) + } + } + } + } } From 9d33dd527f554626b00ef97f422ddbf647897229 Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Sat, 22 Oct 2022 19:15:49 +0530 Subject: [PATCH 5/6] ci: add workflows --- .github/workflows/lint.yml | 15 +++++++++++++++ .github/workflows/tests.yml | 14 ++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..f7e28c6 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,15 @@ +name: Lint +on: [push, pull_request] +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.18 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.48 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e204772 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,14 @@ +on: [push, pull_request] +name: Tests +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Install Go. + uses: actions/setup-go@v2 + with: + go-version: 1.18 + - name: Checkout code. + uses: actions/checkout@v2 + - name: Run tests. + run: go test -v ./... From ac95dbcd1b2d088794a015dcae940ebff2ce2957 Mon Sep 17 00:00:00 2001 From: burntcarrot Date: Sat, 22 Oct 2022 19:24:34 +0530 Subject: [PATCH 6/6] chore: add linter ignore rules --- client/main.go | 4 ++-- tui/tui.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/main.go b/client/main.go index bc22688..3f8803a 100644 --- a/client/main.go +++ b/client/main.go @@ -65,7 +65,7 @@ func main() { // Send joining message. msg := message{Username: name, Text: "has joined the chat.", Type: "info"} - conn.WriteJSON(msg) + _ = conn.WriteJSON(msg) go readMessages(conn) // Handle incoming messages concurrently. writeMessages(conn, s, name) // Handle outgoing messages concurrently. @@ -102,7 +102,7 @@ func writeMessages(conn ConnWriter, s Scanner, name string) { // Handle quit event. if msg.Text == "!q" { fmt.Println("Goodbye!") - conn.WriteJSON(message{Username: name, Text: "has disconnected.", Type: "info"}) + _ = conn.WriteJSON(message{Username: name, Text: "has disconnected.", Type: "info"}) conn.Close() os.Exit(0) } diff --git a/tui/tui.go b/tui/tui.go index 5a8e62b..78aca5a 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -9,7 +9,7 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -func UI() { +func UI() { //nolint:deadcode p := tea.NewProgram(initialModel()) if err := p.Start(); err != nil { log.Fatal(err)