This commit is contained in:
Cereska
2026-03-02 08:47:52 -05:00
parent a1b599d030
commit 661011485c
7 changed files with 184 additions and 42 deletions

1
go.mod
View File

@@ -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
)

2
go.sum
View File

@@ -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=

70
index.html Normal file
View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<style>
#chat-container {
width: 400px;
height: 500px;
border: 1px solid #ccc;
display: flex;
flex-direction: column;
}
#messages {
flex: 1;
overflow-y: auto; /* Enables scrolling */
padding: 10px;
background: #f9f9f9;
}
.msg { margin-bottom: 10px; }
.user { font-weight: bold; color: #2c3e50; }
.time { font-size: 0.7em; color: #999; }
.controls { display: flex; padding: 10px; border-top: 1px solid #eee; }
input { flex: 1; padding: 5px; }
</style>
</head>
<body>
<div id="chat-container">
<div id="messages"></div>
<div class="controls">
<input type="text" id="input" placeholder="Enter message...">
<button onclick="send()">Send</button>
</div>
</div>
<script>
const room = new URLSearchParams(window.location.search).get('room') || 'general';
const ws = new WebSocket(`ws://localhost:8080/ws?room=${room}`);
const msgDiv = document.getElementById('messages');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
appendMessage(data);
};
function appendMessage(data) {
const item = document.createElement('div');
item.className = 'msg';
const time = new Date(data.timestamp).toLocaleTimeString();
item.innerHTML = `
<span class="user">${data.username}:</span>
<span>${data.content}</span>
<div class="time">${time}</div>
`;
msgDiv.appendChild(item);
// Auto-scroll to bottom when a new message arrives
msgDiv.scrollTop = msgDiv.scrollHeight;
}
function send() {
const input = document.getElementById('input');
ws.send(JSON.stringify({
content: input.value,
username: "User123" // In production, get this from auth
}));
input.value = '';
}
</script>
</body>
</html>

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

33
main.go
View File

@@ -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)
}