diff --git a/go.mod b/go.mod index 18ee462..531e062 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module chat go 1.25.0 require ( + github.com/coder/websocket v1.8.14 // indirect github.com/labstack/echo/v5 v5.0.4 // indirect golang.org/x/time v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index aab5e4e..52b5459 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/labstack/echo/v5 v5.0.4 h1:ll3I/O8BifjMztj9dD1vx/peZQv8cR2CTUdQK6QxGGc= github.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= diff --git a/index.html b/index.html new file mode 100644 index 0000000..95bdecb --- /dev/null +++ b/index.html @@ -0,0 +1,70 @@ + + + + + + +
+
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/internal/models/channel.go b/internal/models/channel.go index cc320bc..82a165d 100644 --- a/internal/models/channel.go +++ b/internal/models/channel.go @@ -1,8 +1,11 @@ package models import ( + "context" "sync" "time" + + "github.com/coder/websocket" ) type Channel struct { @@ -10,21 +13,28 @@ type Channel struct { RoomName string `json:"room_name"` TotalMessages int64 `json:"total_messages"` Messages []Message - mu sync.RWMutex + Mu sync.RWMutex + // Active listeners in specific channel + Clients map[*websocket.Conn]bool } type Message struct { - Content string - Sender *User - Timestamp time.Time + Content string `json:"content"` + Sender *User `json:"sender"` + Timestamp time.Time `json:"timestamp"` } -// Methods for channel -func (c *Channel) Send(msg Message) { - // Lock write - c.mu.Lock() - defer c.mu.Unlock() +func (c *Channel) Broadcast(ctx context.Context, msg Message) { + c.Mu.Lock() + defer c.Mu.Unlock() + // Save to history c.Messages = append(c.Messages, msg) c.TotalMessages++ + + // Send to all active WebSocket connections + for conn := range c.Clients { + payload := []byte(msg.Sender.Username + ": " + msg.Content) + _ = conn.Write(ctx, websocket.MessageText, payload) + } } diff --git a/internal/repositories/messaging.go b/internal/repositories/messaging.go index 9d131aa..3f43206 100644 --- a/internal/repositories/messaging.go +++ b/internal/repositories/messaging.go @@ -1,34 +1 @@ package repositories - -import ( - "chat/internal/models" - "fmt" - "time" -) - -func SendChatMessage(msg string, user *models.User, channel *models.Channel) error { - // Handle empty message - if len(msg) < 1 { - return fmt.Errorf("message cannot be empty") - } - - // Handle long message - if len(msg) > 500 { - return fmt.Errorf("message too long") - } - - // Check if user is banned - banned := user.IsBanned - if banned { - return fmt.Errorf("user: %s, is banned", user.ID) - } - - // Send message to channel - channel.Send(models.Message{ - Content: msg, - Sender: user, - Timestamp: time.Now(), - }) - - return nil -} diff --git a/internal/web/handlers.go b/internal/web/handlers.go index efb3895..5a5fbd0 100644 --- a/internal/web/handlers.go +++ b/internal/web/handlers.go @@ -1 +1,60 @@ package web + +import ( + "chat/internal/cache" + "chat/internal/models" + "net/http" + "time" + + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" +) + +func HandleChat(w http.ResponseWriter, r *http.Request, cache *cache.Cache) { + // Get channel id from Cache + roomID := r.URL.Query().Get("room") + val, found := cache.Get(roomID) + if !found { + http.Error(w, "Channel not found", 404) + return + } + ch := val.(*models.Channel) + + // Accept websocket + conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + InsecureSkipVerify: true, + }) + if err != nil { + return + } + defer conn.Close(websocket.StatusNormalClosure, "") + + ctx := r.Context() + + // Register client to this channel + ch.Mu.RLock() + for _, msg := range ch.Messages { + _ = wsjson.Write(ctx, conn, msg) + } + ch.Mu.RUnlock() + + // Register for live updates + ch.Mu.Lock() + if ch.Clients == nil { + ch.Clients = make(map[*websocket.Conn]bool) + } + ch.Clients[conn] = true + ch.Mu.Unlock() + + // Simple read loop + for { + var msg models.Message + err := wsjson.Read(ctx, conn, &msg) + if err != nil { + break + } + + msg.Timestamp = time.Now() + ch.Broadcast(ctx, msg) + } +} diff --git a/main.go b/main.go index 06ab7d0..5e43b16 100644 --- a/main.go +++ b/main.go @@ -1 +1,34 @@ package main + +import ( + "chat/internal/cache" + "chat/internal/models" + "chat/internal/web" + "net/http" + "time" + + "github.com/coder/websocket" +) + +func main() { + // Initialize cache + cache := cache.NewCache(10 * time.Minute) + + // Pregenerate "General" chat room + generalRoom := &models.Channel{ + ID: "general", + RoomName: "General Chat", + Clients: make(map[*websocket.Conn]bool), + } + cache.Set(generalRoom.ID, generalRoom, 24*time.Hour) + + // Define websocket route + http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { + web.HandleChat(w, r, cache) + }) + + // Serve frontend + http.Handle("/", http.FileServer(http.Dir("."))) + + http.ListenAndServe(":8080", nil) +}