Merge dev into main.

This commit is contained in:
emil28092005
2026-01-14 03:06:12 +03:00
-595
View File
@@ -1,595 +0,0 @@
--- cmd/server/main.go ---
package main
import (
"log"
"net/http"
"shagram/internal/api"
"shagram/internal/db"
"shagram/internal/models"
"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()
hub.GetOrCreateRoom("general")
hub.GetOrCreateRoom("chat")
hub.GetOrCreateRoom("dev")
router := gin.Default()
router.GET("/ws/:room", func(c *gin.Context) {
api.WebSocketHandler(hub, database)(c)
})
router.GET("/api/rooms", func(c *gin.Context) {
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) {
c.File("./static/index.html")
})
router.Run(":8080")
}
--- go.mod ---
module shagram
go 1.25.5
require (
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.11.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)
--- go.sum ---
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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 (
"log"
"net/http"
"shagram/internal/db"
"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, database *db.DB) 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
}
user := msg["user"]
if user == "" {
user = "Anonymous"
}
_, 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)
}
}()
}
}
--- 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)
}
}
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
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␍ 2Ñ¢[5 çÂyS-␍Ü␍³␍„␍Z␍/␍ Ø ¯ _ 2+!3chatЭмильку2026-01-13 23:23:54$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 ---
<!DOCTYPE html>
<html>
<head>
<title>Shagram Chat</title>
<style>
body { font-family: Arial; margin: 20px; }
#messages {
height: 300px;
border: 1px solid #ccc;
overflow-y: auto;
padding: 10px;
margin-bottom: 10px;
background: #f9f9f9;
}
.message {
margin: 5px 0;
padding: 8px;
background: #e3f2fd;
border-radius: 5px;
border-left: 3px solid #2196F3;
}
input { padding: 8px; width: 300px; }
button { padding: 8px 15px; cursor: pointer; }
select { padding: 8px; width: 320px; }
#roomsList { margin: 10px 0; }
</style>
</head>
<body>
<h1>📱 Shagram Chat</h1>
<div id="roomsList">
<select id="roomSelect">
<option value="general">general</option>
</select>
<input type="text" id="userInput" placeholder="Your name" value="Эмиль" maxlength="20">
<button onclick="loadRooms()">🔄</button>
<button onclick="connectRoom()">Connect</button>
</div>
<div id="messages"></div>
<div>
<input type="text" id="messageInput" placeholder="Type message..."
onkeypress="if(event.key==='Enter') sendMessage()">
<button onclick="sendMessage()">Send</button>
</div>
<script src="/static/js/chat.js"></script>
</body>
</html>
--- static/js/chat.js ---
let ws = null;
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
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 = 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('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) {
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 += '<div class="message">⌠Error: ' + error + '</div>';
};
ws.onclose = function() {
messagesDiv.innerHTML += '<div class="message">â›” Disconnected</div>';
};
}
function sendMessage() {
const text = messageInput.value.trim();
const usernameInput = document.getElementById('userInput');
const username = usernameInput ? usernameInput.value.trim() || 'Anonymous' : 'Anonymous';
if (!text || !ws || ws.readyState !== WebSocket.OPEN) {
alert('Not connected or empty message');
return;
}
ws.send(JSON.stringify({
text: text,
user: username,
}));
messageInput.value = '';
}
loadRooms();