feat: persist chat messages to SQLite (#1)
All checks were successful
Deploy c4 / deploy (push) Successful in 44s
All checks were successful
Deploy c4 / deploy (push) Successful in 44s
This commit was merged in pull request #1.
This commit is contained in:
71
db/gen/chat.sql.go
Normal file
71
db/gen/chat.sql.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: chat.sql
|
||||||
|
|
||||||
|
package gen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createChatMessage = `-- name: CreateChatMessage :exec
|
||||||
|
INSERT INTO chat_messages (game_id, nickname, color, message, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateChatMessageParams struct {
|
||||||
|
GameID string
|
||||||
|
Nickname string
|
||||||
|
Color int64
|
||||||
|
Message string
|
||||||
|
CreatedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateChatMessage(ctx context.Context, arg CreateChatMessageParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, createChatMessage,
|
||||||
|
arg.GameID,
|
||||||
|
arg.Nickname,
|
||||||
|
arg.Color,
|
||||||
|
arg.Message,
|
||||||
|
arg.CreatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChatMessages = `-- name: GetChatMessages :many
|
||||||
|
SELECT id, game_id, nickname, color, message, created_at FROM chat_messages
|
||||||
|
WHERE game_id = ?
|
||||||
|
ORDER BY created_at DESC, id DESC
|
||||||
|
LIMIT 50
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetChatMessages(ctx context.Context, gameID string) ([]ChatMessage, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getChatMessages, gameID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ChatMessage
|
||||||
|
for rows.Next() {
|
||||||
|
var i ChatMessage
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.GameID,
|
||||||
|
&i.Nickname,
|
||||||
|
&i.Color,
|
||||||
|
&i.Message,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -8,6 +8,15 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ChatMessage struct {
|
||||||
|
ID int64
|
||||||
|
GameID string
|
||||||
|
Nickname string
|
||||||
|
Color int64
|
||||||
|
Message string
|
||||||
|
CreatedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
ID string
|
ID string
|
||||||
Board string
|
Board string
|
||||||
|
|||||||
15
db/migrations/006_add_chat_messages.sql
Normal file
15
db/migrations/006_add_chat_messages.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE chat_messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
game_id TEXT NOT NULL,
|
||||||
|
nickname TEXT NOT NULL,
|
||||||
|
color INTEGER NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_chat_messages_game ON chat_messages(game_id, created_at);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP INDEX IF EXISTS idx_chat_messages_game;
|
||||||
|
DROP TABLE IF EXISTS chat_messages;
|
||||||
@@ -3,10 +3,12 @@ package db
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/ryanhamamura/c4/db/gen"
|
"github.com/ryanhamamura/c4/db/gen"
|
||||||
"github.com/ryanhamamura/c4/game"
|
"github.com/ryanhamamura/c4/game"
|
||||||
"github.com/ryanhamamura/c4/snake"
|
"github.com/ryanhamamura/c4/snake"
|
||||||
|
"github.com/ryanhamamura/c4/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GamePersister struct {
|
type GamePersister struct {
|
||||||
@@ -286,3 +288,40 @@ func (p *SnakePersister) DeleteSnakeGame(id string) error {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
return p.queries.DeleteSnakeGame(ctx, id)
|
return p.queries.DeleteSnakeGame(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChatPersister struct {
|
||||||
|
queries *gen.Queries
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatPersister(q *gen.Queries) *ChatPersister {
|
||||||
|
return &ChatPersister{queries: q}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ChatPersister) SaveChatMessage(gameID string, msg ui.C4ChatMessage) error {
|
||||||
|
return p.queries.CreateChatMessage(context.Background(), gen.CreateChatMessageParams{
|
||||||
|
GameID: gameID,
|
||||||
|
Nickname: msg.Nickname,
|
||||||
|
Color: int64(msg.Color),
|
||||||
|
Message: msg.Message,
|
||||||
|
CreatedAt: msg.Time,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ChatPersister) LoadChatMessages(gameID string) ([]ui.C4ChatMessage, error) {
|
||||||
|
rows, err := p.queries.GetChatMessages(context.Background(), gameID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
msgs := make([]ui.C4ChatMessage, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
msgs[i] = ui.C4ChatMessage{
|
||||||
|
Nickname: r.Nickname,
|
||||||
|
Color: int(r.Color),
|
||||||
|
Message: r.Message,
|
||||||
|
Time: r.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Query returns newest-first; reverse to oldest-first for display
|
||||||
|
slices.Reverse(msgs)
|
||||||
|
return msgs, nil
|
||||||
|
}
|
||||||
|
|||||||
9
db/queries/chat.sql
Normal file
9
db/queries/chat.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- name: CreateChatMessage :exec
|
||||||
|
INSERT INTO chat_messages (game_id, nickname, color, message, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
-- name: GetChatMessages :many
|
||||||
|
SELECT * FROM chat_messages
|
||||||
|
WHERE game_id = ?
|
||||||
|
ORDER BY created_at DESC, id DESC
|
||||||
|
LIMIT 50;
|
||||||
11
main.go
11
main.go
@@ -27,9 +27,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
store = game.NewGameStore()
|
store = game.NewGameStore()
|
||||||
snakeStore = snake.NewSnakeStore()
|
snakeStore = snake.NewSnakeStore()
|
||||||
queries *gen.Queries
|
queries *gen.Queries
|
||||||
|
chatPersister *db.ChatPersister
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed assets
|
//go:embed assets
|
||||||
@@ -61,6 +62,7 @@ func main() {
|
|||||||
queries = gen.New(db.DB)
|
queries = gen.New(db.DB)
|
||||||
store.SetPersister(db.NewGamePersister(queries))
|
store.SetPersister(db.NewGamePersister(queries))
|
||||||
snakeStore.SetPersister(db.NewSnakePersister(queries))
|
snakeStore.SetPersister(db.NewSnakePersister(queries))
|
||||||
|
chatPersister = db.NewChatPersister(queries)
|
||||||
|
|
||||||
sessionManager, err := via.NewSQLiteSessionManager(db.DB)
|
sessionManager, err := via.NewSQLiteSessionManager(db.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -356,7 +358,7 @@ func main() {
|
|||||||
colSignal := c.Signal(0)
|
colSignal := c.Signal(0)
|
||||||
showGuestPrompt := c.Signal(false)
|
showGuestPrompt := c.Signal(false)
|
||||||
chatMsg := c.Signal("")
|
chatMsg := c.Signal("")
|
||||||
var chatMessages []ui.C4ChatMessage
|
chatMessages, _ := chatPersister.LoadChatMessages(gameID)
|
||||||
var chatMu sync.Mutex
|
var chatMu sync.Mutex
|
||||||
|
|
||||||
goToLogin := c.Action(func() {
|
goToLogin := c.Action(func() {
|
||||||
@@ -462,6 +464,7 @@ func main() {
|
|||||||
Message: msg,
|
Message: msg,
|
||||||
Time: time.Now().UnixMilli(),
|
Time: time.Now().UnixMilli(),
|
||||||
}
|
}
|
||||||
|
chatPersister.SaveChatMessage(gameID, cm)
|
||||||
data, err := json.Marshal(cm)
|
data, err := json.Marshal(cm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user