diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..a034499 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + "shagram/internal/api" + "shagram/internal/db" + "shagram/internal/websocket" + + "github.com/gin-gonic/gin" +) + +func main() { + database, err := db.NewDB("shagram.db") + if err != nil { + log.Fatal(err) + } + defer database.Close() + hub := websocket.NewHub() + router := gin.Default() + + router.GET("/ws/:room", api.WebSocketHandler(hub)) + router.GET("/api/rooms", func(c *gin.Context) { + c.JSON(200, gin.H{"rooms": "TODO"}) + }) + router.Run(":8080") +} diff --git a/internal/api/websocket.go b/internal/api/websocket.go new file mode 100644 index 0000000..eefba1c --- /dev/null +++ b/internal/api/websocket.go @@ -0,0 +1,46 @@ +package api + +import ( + "net/http" + "shagram/internal/websocket" + + "github.com/gin-gonic/gin" + gws "github.com/gorilla/websocket" +) + +var upgrader = gws.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, +} + +func WebSocketHandler(hub *websocket.Hub) gin.HandlerFunc { + return func(c *gin.Context) { + roomID := c.Param("room") + room := hub.GetOrCreateRoom(roomID) + + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + + client := &websocket.Client{Conn: conn, Room: room} + room.Register(client) + + go func() { + defer func() { + room.Unregister(client) + hub.CleanupRoom(roomID) + }() + + for { + var msg map[string]string + err := client.Conn.ReadJSON(&msg) + if err != nil { + break + } + message := []byte(msg["text"]) + room.Broadcast(message) + } + }() + } + +} diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go index 9d0795d..727c294 100644 --- a/internal/db/sqlite.go +++ b/internal/db/sqlite.go @@ -17,7 +17,7 @@ func NewDB(path string) (*DB, error) { return nil, err } - schema, err := os.ReadFile("../../migrations/schema.sql") + schema, err := os.ReadFile("migrations/schema.sql") if err != nil { return nil, err } diff --git a/internal/websocket/room.go b/internal/websocket/room.go index f98d340..a69e59e 100644 --- a/internal/websocket/room.go +++ b/internal/websocket/room.go @@ -3,8 +3,8 @@ package websocket import "github.com/gorilla/websocket" type Client struct { - conn *websocket.Conn - room *Room + Conn *websocket.Conn + Room *Room } type Room struct { @@ -25,12 +25,12 @@ func (r *Room) Register(client *Client) { func (r *Room) Unregister(client *Client) { delete(r.clients, client) - client.conn.Close() + client.Conn.Close() } func (r *Room) Broadcast(message []byte) { for client := range r.clients { - err := client.conn.WriteMessage(websocket.TextMessage, message) + err := client.Conn.WriteMessage(websocket.TextMessage, message) if err != nil { r.Unregister(client) } diff --git a/project_context.txt b/project_context.txt index e8461d9..50d4c7f 100644 --- a/project_context.txt +++ b/project_context.txt @@ -1,3 +1,31 @@ +--- cmd/server/main.go --- +package main + +import ( + "log" + "shagram/internal/api" + "shagram/internal/db" + "shagram/internal/websocket" + + "github.com/gin-gonic/gin" +) + +func main() { + database, err := db.NewDB("shagram.db") + if err != nil { + log.Fatal(err) + } + defer database.Close() + hub := websocket.NewHub() + router := gin.Default() + + router.GET("/ws/:room", api.WebSocketHandler(hub)) + router.GET("/api/rooms", func(c *gin.Context) { + c.JSON(200, gin.H{"rooms": "TODO"}) + }) + router.Run(":8080") +} + --- go.mod --- module shagram @@ -125,3 +153,199 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +--- internal/api/websocket.go --- +package api + +import ( + "net/http" + "shagram/internal/websocket" + + "github.com/gin-gonic/gin" + gws "github.com/gorilla/websocket" +) + +var upgrader = gws.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, +} + +func WebSocketHandler(hub *websocket.Hub) gin.HandlerFunc { + return func(c *gin.Context) { + roomID := c.Param("room") + room := hub.GetOrCreateRoom(roomID) + + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + + client := &websocket.Client{Conn: conn, Room: room} + room.Register(client) + + go func() { + defer func() { + room.Unregister(client) + hub.CleanupRoom(roomID) + }() + + for { + var msg map[string]string + err := client.Conn.ReadJSON(&msg) + if err != nil { + break + } + message := []byte(msg["text"]) + room.Broadcast(message) + } + }() + } + +} + +--- internal/db/sqlite.go --- +package db + +import ( + "database/sql" + "os" + + _ "github.com/mattn/go-sqlite3" +) + +type DB struct { + *sql.DB +} + +func NewDB(path string) (*DB, error) { + db, err := sql.Open("sqlite3", path) + if err != nil { + return nil, err + } + + schema, err := os.ReadFile("migrations/schema.sql") + if err != nil { + return nil, err + } + + _, err = db.Exec(string(schema)) + if err != nil { + return nil, err + } + + return &DB{db}, nil +} + +--- internal/models/message.go --- +package models + +import "time" + +type Message struct { + ID int `json:"id"` + RoomID string `json:"room_id"` + User string `json:"user"` + Text string `json:"text"` + CreatedAt time.Time `json:"created_at"` +} + +type Room struct { + ID string `json:"id"` + Name string `json:"name"` +} + +--- internal/websocket/hub.go --- +package websocket + +type Hub struct { + rooms map[string]*Room +} + +func NewHub() *Hub { + return &Hub{ + rooms: make(map[string]*Room), + } +} + +func (h *Hub) GetOrCreateRoom(roomID string) *Room { + if _, ok := h.rooms[roomID]; !ok { + h.rooms[roomID] = NewRoom(roomID) + } + return h.rooms[roomID] +} + +func (h *Hub) CleanupRoom(roomID string) { + room := h.rooms[roomID] + if len(room.clients) == 0 { + delete(h.rooms, roomID) + } +} + +--- internal/websocket/room.go --- +package websocket + +import "github.com/gorilla/websocket" + +type Client struct { + Conn *websocket.Conn + Room *Room +} + +type Room struct { + id string + clients map[*Client]bool +} + +func NewRoom(id string) *Room { + return &Room{ + id: id, + clients: make(map[*Client]bool), + } +} + +func (r *Room) Register(client *Client) { + r.clients[client] = true +} + +func (r *Room) Unregister(client *Client) { + delete(r.clients, client) + client.Conn.Close() +} + +func (r *Room) Broadcast(message []byte) { + for client := range r.clients { + err := client.Conn.WriteMessage(websocket.TextMessage, message) + if err != nil { + r.Unregister(client) + } + } +} + +--- migrations/schema.sql --- +CREATE TABLE IF NOT EXISTS rooms ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT NOT NULL, + user TEXT NOT NULL, + text TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (room_id) REFERENCES rooms (id) +); + +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 ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT NOT NULL, + user TEXT NOT NULL, + text TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (room_id) REFERENCES rooms (id) +)]tableroomsroomsCREATE TABLE rooms ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL +))=indexsqlite_autoindex_rooms_1rooms generalGeneral + + general   diff --git a/shagram.db b/shagram.db new file mode 100644 index 0000000..acd7715 Binary files /dev/null and b/shagram.db differ diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..97921a1 --- /dev/null +++ b/static/index.html @@ -0,0 +1,44 @@ + + + + Shagram Chat + + + +

📱 Shagram Chat

+ +
+ + +
+ +
+ +
+ + +
+ + + + diff --git a/static/js/chat.js b/static/js/chat.js new file mode 100644 index 0000000..ca42810 --- /dev/null +++ b/static/js/chat.js @@ -0,0 +1,43 @@ +let ws = null; +const messagesDiv = document.getElementById('messages'); +const messageInput = document.getElementById('messageInput'); +const roomInput = document.getElementById('roomInput'); + +function connectRoom() { + const room = roomInput.value || 'general'; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + ws = new WebSocket(`${protocol}//localhost:8080/ws/${room}`); + + ws.onopen = function() { + console.log('Connected to room: ' + room); + messagesDiv.innerHTML = '
✅ Connected to ' + room + '
'; + }; + + ws.onmessage = function(event) { + const msgDiv = document.createElement('div'); + msgDiv.className = 'message'; + msgDiv.textContent = event.data; + messagesDiv.appendChild(msgDiv); + messagesDiv.scrollTop = messagesDiv.scrollHeight; + }; + + ws.onerror = function(error) { + console.error('WebSocket error:', error); + messagesDiv.innerHTML += '
❌ Error: ' + error + '
'; + }; + + ws.onclose = function() { + messagesDiv.innerHTML += '
⛔ Disconnected
'; + }; +} + +function sendMessage() { + const text = messageInput.value.trim(); + if (!text || !ws || ws.readyState !== WebSocket.OPEN) { + alert('Not connected or empty message'); + return; + } + + ws.send(JSON.stringify({text: text})); + messageInput.value = ''; +} \ No newline at end of file