Port the FastAPI backend to Go 1.26, preserving all API functionality: - auth (register/login/guest/logout/profile/history) with JWT + bcrypt - scripts (list/detail) backed by embedded sample_data.json - games (create/join/start/chat/phase/clue/vote/state/replay) + WebSocket - rule-based NPC agent plus DeepSeek/Ollama AI backends - SQLite persistence via modernc.org/sqlite (pure Go, no cgo) Frontend and documents are unchanged.
219 lines
5.6 KiB
Go
219 lines
5.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"mmgame/internal/database"
|
|
"mmgame/internal/models"
|
|
"mmgame/internal/scripts"
|
|
"mmgame/internal/util"
|
|
)
|
|
|
|
func newID() string {
|
|
b := make([]byte, 16)
|
|
_, _ = rand.Read(b)
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, detail string) {
|
|
writeJSON(w, status, map[string]string{"detail": detail})
|
|
}
|
|
|
|
func nullStr(s *string) string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return *s
|
|
}
|
|
|
|
func Health(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, 200, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func parseConfig(configStr string) map[string]interface{} {
|
|
if configStr == "" {
|
|
return map[string]interface{}{}
|
|
}
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal([]byte(configStr), &m); err != nil {
|
|
return map[string]interface{}{}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func scriptFromConfig(configStr string) *scripts.Script {
|
|
cfg := parseConfig(configStr)
|
|
raw, ok := cfg["script"]
|
|
if !ok || raw == nil {
|
|
return nil
|
|
}
|
|
b, err := json.Marshal(raw)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var s scripts.Script
|
|
if err := json.Unmarshal(b, &s); err != nil {
|
|
return nil
|
|
}
|
|
return &s
|
|
}
|
|
|
|
func getGame(gameID string) (*models.GameSession, error) {
|
|
row := database.DB.QueryRow(
|
|
`SELECT id, script_id, status, phase, phase_index, started_at, completed_at, config, created_at
|
|
FROM game_sessions WHERE id = ?`, gameID)
|
|
var g models.GameSession
|
|
var startedAt, completedAt, configStr, createdAt sql.NullString
|
|
err := row.Scan(&g.ID, &g.ScriptID, &g.Status, &g.Phase, &g.PhaseIndex,
|
|
&startedAt, &completedAt, &configStr, &createdAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if startedAt.Valid {
|
|
t := util.ParseTime(startedAt.String)
|
|
g.StartedAt = &t
|
|
}
|
|
if completedAt.Valid {
|
|
t := util.ParseTime(completedAt.String)
|
|
g.CompletedAt = &t
|
|
}
|
|
g.Config = configStr.String
|
|
g.CreatedAt = util.ParseTime(createdAt.String)
|
|
return &g, nil
|
|
}
|
|
|
|
func queryPlayers(gameID string) []*models.SessionPlayer {
|
|
rows, err := database.DB.Query(
|
|
`SELECT id, session_id, user_id, role_id, role_name, is_human, is_ready, avatar_url, status, joined_at
|
|
FROM session_players WHERE session_id = ?`, gameID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer rows.Close()
|
|
var list []*models.SessionPlayer
|
|
for rows.Next() {
|
|
p := &models.SessionPlayer{}
|
|
var userID, avatarURL, joinedAt sql.NullString
|
|
if err := rows.Scan(&p.ID, &p.SessionID, &userID, &p.RoleID, &p.RoleName,
|
|
&p.IsHuman, &p.IsReady, &avatarURL, &p.Status, &joinedAt); err != nil {
|
|
continue
|
|
}
|
|
if userID.Valid {
|
|
p.UserID = &userID.String
|
|
}
|
|
if avatarURL.Valid {
|
|
p.AvatarURL = &avatarURL.String
|
|
}
|
|
p.JoinedAt = util.ParseTime(joinedAt.String)
|
|
list = append(list, p)
|
|
}
|
|
return list
|
|
}
|
|
|
|
func queryMessages(gameID string) []*models.ChatMessage {
|
|
rows, err := database.DB.Query(
|
|
`SELECT id, session_id, sender_role_id, sender_name, message_type, content, target_role_id, clue_id, phase_index, created_at
|
|
FROM chat_messages WHERE session_id = ? ORDER BY created_at`, gameID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer rows.Close()
|
|
var list []*models.ChatMessage
|
|
for rows.Next() {
|
|
m := &models.ChatMessage{}
|
|
var targetRoleID, clueID, createdAt sql.NullString
|
|
if err := rows.Scan(&m.ID, &m.SessionID, &m.SenderRoleID, &m.SenderName, &m.MessageType,
|
|
&m.Content, &targetRoleID, &clueID, &m.PhaseIndex, &createdAt); err != nil {
|
|
continue
|
|
}
|
|
if targetRoleID.Valid {
|
|
m.TargetRoleID = &targetRoleID.String
|
|
}
|
|
if clueID.Valid {
|
|
m.ClueID = &clueID.String
|
|
}
|
|
m.CreatedAt = util.ParseTime(createdAt.String)
|
|
list = append(list, m)
|
|
}
|
|
return list
|
|
}
|
|
|
|
func queryClues(gameID string) []*models.ClueState {
|
|
rows, err := database.DB.Query(
|
|
`SELECT id, session_id, clue_id, status, revealed_by, released_at, created_at
|
|
FROM clues_state WHERE session_id = ?`, gameID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer rows.Close()
|
|
var list []*models.ClueState
|
|
for rows.Next() {
|
|
c := &models.ClueState{}
|
|
var revealedBy, releasedAt, createdAt sql.NullString
|
|
if err := rows.Scan(&c.ID, &c.SessionID, &c.ClueID, &c.Status, &revealedBy, &releasedAt, &createdAt); err != nil {
|
|
continue
|
|
}
|
|
if revealedBy.Valid {
|
|
c.RevealedBy = &revealedBy.String
|
|
}
|
|
if releasedAt.Valid {
|
|
t := util.ParseTime(releasedAt.String)
|
|
c.ReleasedAt = &t
|
|
}
|
|
c.CreatedAt = util.ParseTime(createdAt.String)
|
|
list = append(list, c)
|
|
}
|
|
return list
|
|
}
|
|
|
|
func queryVotes(gameID string) []*models.Vote {
|
|
rows, err := database.DB.Query(
|
|
`SELECT id, session_id, round, voter_id, target_id, reason, created_at
|
|
FROM votes WHERE session_id = ?`, gameID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer rows.Close()
|
|
var list []*models.Vote
|
|
for rows.Next() {
|
|
v := &models.Vote{}
|
|
var reason, createdAt sql.NullString
|
|
if err := rows.Scan(&v.ID, &v.SessionID, &v.Round, &v.VoterID, &v.TargetID, &reason, &createdAt); err != nil {
|
|
continue
|
|
}
|
|
if reason.Valid {
|
|
v.Reason = &reason.String
|
|
}
|
|
v.CreatedAt = util.ParseTime(createdAt.String)
|
|
list = append(list, v)
|
|
}
|
|
return list
|
|
}
|
|
|
|
func messageDict(m *models.ChatMessage) map[string]interface{} {
|
|
d := map[string]interface{}{
|
|
"id": m.ID,
|
|
"sender_role_id": m.SenderRoleID,
|
|
"sender_name": m.SenderName,
|
|
"message_type": m.MessageType,
|
|
"content": m.Content,
|
|
"target_role_id": m.TargetRoleID,
|
|
"clue_id": m.ClueID,
|
|
"phase_index": m.PhaseIndex,
|
|
"created_at": util.TimeStr(m.CreatedAt),
|
|
}
|
|
return d
|
|
}
|