From 975aea12c96ade51cc1a52c16d8dae186d733d1d Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Tue, 13 Jan 2026 01:07:56 +0300 Subject: [PATCH] SHAGRAM 1.0 --- .gitignore | 1 + cmd/server/main.go | 62 ++++++++++++++- internal/api/websocket.go | 11 ++- internal/websocket/hub.go | 8 ++ project_context.txt | 163 +++++++++++++++++++++++++++++++++++--- shagram.db | Bin 20480 -> 20480 bytes static/index.html | 10 ++- static/js/chat.js | 69 ++++++++++++++-- 8 files changed, 301 insertions(+), 23 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e15f080 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +project_context.txt \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 7ad9a18..dad0f38 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,8 +2,10 @@ package main import ( "log" + "net/http" "shagram/internal/api" "shagram/internal/db" + "shagram/internal/models" "shagram/internal/websocket" "github.com/gin-gonic/gin" @@ -16,11 +18,67 @@ func main() { } defer database.Close() hub := websocket.NewHub() + hub.GetOrCreateRoom("general") + hub.GetOrCreateRoom("chat") + hub.GetOrCreateRoom("dev") + router := gin.Default() - router.GET("/ws/:room", api.WebSocketHandler(hub)) + router.GET("/ws/:room", func(c *gin.Context) { + api.WebSocketHandler(hub, database)(c) + }) router.GET("/api/rooms", func(c *gin.Context) { - c.JSON(200, gin.H{"rooms": "TODO"}) + rows, err := database.Query(` + SELECT DISTINCT room_id + FROM messages + ORDER BY room_id`) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var rooms []string + for rows.Next() { + var roomID string + err := rows.Scan(&roomID) + if err != nil { + continue + } + rooms = append(rooms, roomID) + } + + if len(rooms) == 0 { + rooms = []string{"general", "chat", "dev"} + } + + c.JSON(200, gin.H{"rooms": rooms}) + }) + router.GET("/api/messages/:room", func(c *gin.Context) { + roomID := c.Param("room") + rows, err := database.Query(` + SELECT id, room_id, user, text, created_at + FROM messages + WHERE room_id = ? + ORDER BY created_at DESC + LIMIT 50`, roomID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + var messages []models.Message + for rows.Next() { + var msg models.Message + err := rows.Scan(&msg.ID, &msg.RoomID, &msg.User, &msg.Text, &msg.CreatedAt) + if err != nil { + log.Printf("Scan error: %v", err) + continue + } + messages = append(messages, msg) + } + + c.JSON(200, gin.H{"messages": messages}) }) router.Static("/static", "./static") router.GET("/", func(c *gin.Context) { diff --git a/internal/api/websocket.go b/internal/api/websocket.go index eefba1c..c3e87cb 100644 --- a/internal/api/websocket.go +++ b/internal/api/websocket.go @@ -1,7 +1,9 @@ package api import ( + "log" "net/http" + "shagram/internal/db" "shagram/internal/websocket" "github.com/gin-gonic/gin" @@ -12,7 +14,7 @@ var upgrader = gws.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } -func WebSocketHandler(hub *websocket.Hub) gin.HandlerFunc { +func WebSocketHandler(hub *websocket.Hub, database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { roomID := c.Param("room") room := hub.GetOrCreateRoom(roomID) @@ -37,6 +39,13 @@ func WebSocketHandler(hub *websocket.Hub) gin.HandlerFunc { if err != nil { break } + _, err = database.Exec(` + INSERT INTO messages (room_id, user, text) + VALUES (?, ?, ?)`, roomID, "user", msg["text"]) + if err != nil { + log.Printf("Save message error: %v", err) + } + message := []byte(msg["text"]) room.Broadcast(message) } diff --git a/internal/websocket/hub.go b/internal/websocket/hub.go index 375521c..aa36110 100644 --- a/internal/websocket/hub.go +++ b/internal/websocket/hub.go @@ -23,3 +23,11 @@ func (h *Hub) CleanupRoom(roomID string) { delete(h.rooms, roomID) } } + +func (h *Hub) Rooms() []string { + ids := make([]string, 0, len(h.rooms)) + for id := range h.rooms { + ids = append(ids, id) + } + return ids +} diff --git a/project_context.txt b/project_context.txt index 443baeb..3b8241b 100644 --- a/project_context.txt +++ b/project_context.txt @@ -3,8 +3,10 @@ package main import ( "log" + "net/http" "shagram/internal/api" "shagram/internal/db" + "shagram/internal/models" "shagram/internal/websocket" "github.com/gin-gonic/gin" @@ -17,11 +19,67 @@ func main() { } defer database.Close() hub := websocket.NewHub() + hub.GetOrCreateRoom("general") + hub.GetOrCreateRoom("chat") + hub.GetOrCreateRoom("dev") + router := gin.Default() - router.GET("/ws/:room", api.WebSocketHandler(hub)) + router.GET("/ws/:room", func(c *gin.Context) { + api.WebSocketHandler(hub, database)(c) + }) router.GET("/api/rooms", func(c *gin.Context) { - c.JSON(200, gin.H{"rooms": "TODO"}) + rows, err := database.Query(` + SELECT DISTINCT room_id + FROM messages + ORDER BY room_id`) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + + var rooms []string + for rows.Next() { + var roomID string + err := rows.Scan(&roomID) + if err != nil { + continue + } + rooms = append(rooms, roomID) + } + + if len(rooms) == 0 { + rooms = []string{"general", "chat", "dev"} + } + + c.JSON(200, gin.H{"rooms": rooms}) + }) + router.GET("/api/messages/:room", func(c *gin.Context) { + roomID := c.Param("room") + rows, err := database.Query(` + SELECT id, room_id, user, text, created_at + FROM messages + WHERE room_id = ? + ORDER BY created_at DESC + LIMIT 50`, roomID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer rows.Close() + var messages []models.Message + for rows.Next() { + var msg models.Message + err := rows.Scan(&msg.ID, &msg.RoomID, &msg.User, &msg.Text, &msg.CreatedAt) + if err != nil { + log.Printf("Scan error: %v", err) + continue + } + messages = append(messages, msg) + } + + c.JSON(200, gin.H{"messages": messages}) }) router.Static("/static", "./static") router.GET("/", func(c *gin.Context) { @@ -161,7 +219,9 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= package api import ( + "log" "net/http" + "shagram/internal/db" "shagram/internal/websocket" "github.com/gin-gonic/gin" @@ -172,7 +232,7 @@ var upgrader = gws.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } -func WebSocketHandler(hub *websocket.Hub) gin.HandlerFunc { +func WebSocketHandler(hub *websocket.Hub, database *db.DB) gin.HandlerFunc { return func(c *gin.Context) { roomID := c.Param("room") room := hub.GetOrCreateRoom(roomID) @@ -197,6 +257,13 @@ func WebSocketHandler(hub *websocket.Hub) gin.HandlerFunc { if err != nil { break } + _, err = database.Exec(` + INSERT INTO messages (room_id, user, text) + VALUES (?, ?, ?)`, roomID, "user", msg["text"]) + if err != nil { + log.Printf("Save message error: %v", err) + } + message := []byte(msg["text"]) room.Broadcast(message) } @@ -283,6 +350,14 @@ func (h *Hub) CleanupRoom(roomID string) { } } +func (h *Hub) Rooms() []string { + ids := make([]string, 0, len(h.rooms)) + for id := range h.rooms { + ids = append(ids, id) + } + return ids +} + --- internal/websocket/room.go --- package websocket @@ -340,7 +415,7 @@ CREATE TABLE IF NOT EXISTS messages ( INSERT OR IGNORE INTO rooms (id, name) VALUES ('general', 'General'); --- shagram.db --- -SQLite format 3@ .  n] P++Ytablesqlite_sequencesqlite_sequenceCREATE TABLE sqlite_sequence(name,seq)otablemessagesmessagesCREATE TABLE messages ( +SQLite format 3@ .  n] P++Ytablesqlite_sequencesqlite_sequenceCREATE TABLE sqlite_sequence(name,seq)otablemessagesmessagesCREATE TABLE messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, room_id TEXT NOT NULL, user TEXT NOT NULL, @@ -352,7 +427,8 @@ SQLite format 3@  name TEXT NOT NULL ))=indexsqlite_autoindex_rooms_1rooms generalGeneral  - general   + general  _[5 yS- Z /  _$3chatuserAAA2026-01-12 21:59:44(3chatuserdfgsadf2026-01-12 21:59:35'3devusertest DB2026-01-12 21:58:42,#3chatusersefsefsefes2026-01-12 21:43:38'3chatusersefesf2026-01-12 21:43:35)3chatuserdfbzgdfg2026-01-12 21:43:26(3chatuserkuykfgh2026-01-12 21:42:52-3generaluserawfawdfwa2026-01-12 21:28:44'3devuserwadawdw2026-01-12 21:28:40&3chatuserefawe2026-01-12 21:28:16' 3chatuserawdwad2026-01-12 21:27:58$ 3chatuserawd2026-01-12 21:27:25$ 3chatuseradw2026-01-12 21:27:23" +3chatuserd2026-01-12 21:27:20# 3chatuserda2026-01-12 21:27:16#3chatuserku2026-01-12 21:26:58#3chatuserhi2026-01-12 21:26:43'3generaluserASD2026-01-12 21:16:37$3chatuserASD2026-01-12 21:16:30"3devuserHI2026-01-12 21:16:08!3devuser12026-01-12 20:59:23-'3devuserHi from DB! 22026-01-12 20:58:43-'3devuserHi from DB! 12026-01-12 20:58:34  messages --- static/index.html --- @@ -377,13 +453,19 @@ SQLite format 3@  } input { padding: 8px; width: 300px; } button { padding: 8px 15px; cursor: pointer; } + select { padding: 8px; width: 320px; } /* ← для комнат */ + #roomsList { margin: 10px 0; }

📱 Shagram Chat

-
- + +
+ +
@@ -403,16 +485,71 @@ SQLite format 3@  let ws = null; const messagesDiv = document.getElementById('messages'); const messageInput = document.getElementById('messageInput'); -const roomInput = document.getElementById('roomInput'); +const roomSelect = document.getElementById('roomSelect'); + +function loadRooms() { + fetch('/api/rooms') + .then(response => response.json()) + .then(data => { + roomSelect.innerHTML = ''; + data.rooms.forEach(room => { + const option = document.createElement('option'); + option.value = room; + option.textContent = room; + roomSelect.appendChild(option); + }); + }) + .catch(error => { + console.error('Error loading rooms:', error); + }) +} function connectRoom() { - const room = roomInput.value || 'general'; + const room = roomSelect.value || 'general'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + + if (ws) { + ws.close(); + } + ws = new WebSocket(`${protocol}//localhost:8080/ws/${room}`); + + ws.onopen = function() { - console.log('Connected to room: ' + room); - messagesDiv.innerHTML = '
✅ Connected to ' + room + '
'; + console.log('WebSocket connected, loading history for', room); + + fetch(`/api/messages/${room}`) + .then(response => { + console.log('API response:', response.status); + return response.json() + }) + .then(data => { + console.log('History data:', data); + messagesDiv.innerHTML = ''; + const history = data.messages || []; + console.log('history length:', history.length); + if (history.length > 0) { + history.reverse().forEach((msg, index) => { + console.log(`Message ${index}:`, msg) + const msgDiv = document.createElement('div'); + msgDiv.className = 'message'; + msgDiv.textContent = `${msg.user}: ${msg.text}`; + messagesDiv.appendChild(msgDiv); + }) + } + const connectMsg = document.createElement('div'); + connectMsg.className = 'message'; + connectMsg.textContent = `✅ Connected to ${room}`; + messagesDiv.appendChild(connectMsg); + messagesDiv.scrollTop = messagesDiv.scrollHeight; + console.log('History loaded!'); + }) + .catch(error => { + console.error('❌ History error', error); + }) + + }; ws.onmessage = function(event) { @@ -429,7 +566,7 @@ function connectRoom() { }; ws.onclose = function() { - messagesDiv.innerHTML += '
⛔ Disconnected
'; + messagesDiv.innerHTML += '
⛔ Disconnected
'; }; } @@ -443,3 +580,5 @@ function sendMessage() { ws.send(JSON.stringify({text: text})); messageInput.value = ''; } + +loadRooms(); diff --git a/shagram.db b/shagram.db index acd77158154b5b8f435eae2c0e25f3227b596275..be24cb0a1c60bbf7bc08178252d290e71b033ca5 100644 GIT binary patch literal 20480 zcmeI)&2QRf90zba5Ykexo28l6qG(T>Xn-0vuz@g3ljbH)ibhBVj_6CjhuwDAeWzV2P1>=~V_t$ydY)E&AKCE>U;q3(ADe>{EA>w* z?ExjtUcX}xh$h??L{a#N5J3>`v3Z%z)3(G`!qW}56zA4ow|Y-_?}s~a`ClO{-4f)k z{G0TIt)M{w0uX=z1Rwwb2tWV=5P-mcDR3N)C=VZsrvv-xC3T-&vgg8vOP>vC_bj-% zyjwF3%OsZZsA7^pHr2H|bekQS{zeL|C>tB%w>__Bhq|uaqV9AV_EnkgC8;Q3t?iI< z)iMvv8ab|&4~^O>`Pe)q#))-QuCfCU&8oHSCHuWzr@`Wu`Kd*!M{GW+RK_0-UE05H z|A0OpT(|$MPwfG98thSckB!eNADU#(+&4}t7TG3d}rM<4v)vD*gvY7<%6o% zxzwa9X;L%yO_ts@>xA!fNviFnqvD6+#c|ku?@pevuZJu&1G^b zQFD6X13jNlz0FgJS;}d)T-#|bB(*|vJtC~Jq(i^prdx3T`MKmc>3;qn!17W^I#$U77uRBf==`4mo3)ag_>$Hwdx<##qh;NvvnRw zs(L}q+>Qu|^>?(%BN-oU;iRuBdA}Tg$xKmBY2E?1M1B zPOH;<^>KNjQnqwI#53|VlberZ`Ikf0GD|#?oJJnENweSUu)F>~Q3G*ycW4>$weeu} xlCI^)@BhCPWae_1>^F$eEM&^wPOZ1r-1U3sQJmH@>L5z);0SMq+03g;38vp

📱 Shagram Chat

-
- + +
+ +
diff --git a/static/js/chat.js b/static/js/chat.js index ca42810..b21edd4 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -1,16 +1,71 @@ let ws = null; const messagesDiv = document.getElementById('messages'); const messageInput = document.getElementById('messageInput'); -const roomInput = document.getElementById('roomInput'); +const roomSelect = document.getElementById('roomSelect'); + +function loadRooms() { + fetch('/api/rooms') + .then(response => response.json()) + .then(data => { + roomSelect.innerHTML = ''; + data.rooms.forEach(room => { + const option = document.createElement('option'); + option.value = room; + option.textContent = room; + roomSelect.appendChild(option); + }); + }) + .catch(error => { + console.error('Error loading rooms:', error); + }) +} function connectRoom() { - const room = roomInput.value || 'general'; + const room = roomSelect.value || 'general'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + + if (ws) { + ws.close(); + } + ws = new WebSocket(`${protocol}//localhost:8080/ws/${room}`); + + ws.onopen = function() { - console.log('Connected to room: ' + room); - messagesDiv.innerHTML = '
✅ Connected to ' + room + '
'; + console.log('WebSocket connected, loading history for', room); + + fetch(`/api/messages/${room}`) + .then(response => { + console.log('API response:', response.status); + return response.json() + }) + .then(data => { + console.log('History data:', data); + messagesDiv.innerHTML = ''; + const history = data.messages || []; + console.log('history length:', history.length); + if (history.length > 0) { + history.reverse().forEach((msg, index) => { + console.log(`Message ${index}:`, msg) + const msgDiv = document.createElement('div'); + msgDiv.className = 'message'; + msgDiv.textContent = `${msg.user}: ${msg.text}`; + messagesDiv.appendChild(msgDiv); + }) + } + const connectMsg = document.createElement('div'); + connectMsg.className = 'message'; + connectMsg.textContent = `✅ Connected to ${room}`; + messagesDiv.appendChild(connectMsg); + messagesDiv.scrollTop = messagesDiv.scrollHeight; + console.log('History loaded!'); + }) + .catch(error => { + console.error('❌ History error', error); + }) + + }; ws.onmessage = function(event) { @@ -27,7 +82,7 @@ function connectRoom() { }; ws.onclose = function() { - messagesDiv.innerHTML += '
⛔ Disconnected
'; + messagesDiv.innerHTML += '
⛔ Disconnected
'; }; } @@ -40,4 +95,6 @@ function sendMessage() { ws.send(JSON.stringify({text: text})); messageInput.value = ''; -} \ No newline at end of file +} + +loadRooms(); \ No newline at end of file