update
This commit is contained in:
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ module chat
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
github.com/labstack/echo/v5 v5.0.4 // indirect
|
github.com/labstack/echo/v5 v5.0.4 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
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 h1:ll3I/O8BifjMztj9dD1vx/peZQv8cR2CTUdQK6QxGGc=
|
||||||
github.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
|
github.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
|||||||
70
index.html
Normal file
70
index.html
Normal 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>
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
@@ -10,21 +13,28 @@ type Channel struct {
|
|||||||
RoomName string `json:"room_name"`
|
RoomName string `json:"room_name"`
|
||||||
TotalMessages int64 `json:"total_messages"`
|
TotalMessages int64 `json:"total_messages"`
|
||||||
Messages []Message
|
Messages []Message
|
||||||
mu sync.RWMutex
|
Mu sync.RWMutex
|
||||||
|
// Active listeners in specific channel
|
||||||
|
Clients map[*websocket.Conn]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Content string
|
Content string `json:"content"`
|
||||||
Sender *User
|
Sender *User `json:"sender"`
|
||||||
Timestamp time.Time
|
Timestamp time.Time `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods for channel
|
func (c *Channel) Broadcast(ctx context.Context, msg Message) {
|
||||||
func (c *Channel) Send(msg Message) {
|
c.Mu.Lock()
|
||||||
// Lock write
|
defer c.Mu.Unlock()
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
|
// Save to history
|
||||||
c.Messages = append(c.Messages, msg)
|
c.Messages = append(c.Messages, msg)
|
||||||
c.TotalMessages++
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1 @@
|
|||||||
package repositories
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +1,60 @@
|
|||||||
package web
|
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
33
main.go
@@ -1 +1,34 @@
|
|||||||
package main
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user