shotgun/server.go

235 lines
6.5 KiB
Go
Raw Normal View History

2024-01-22 20:16:10 -06:00
package main
import (
"context"
"encoding/json"
2024-01-22 20:16:10 -06:00
"errors"
2024-01-27 13:51:20 -06:00
"log/slog"
2024-01-22 20:16:10 -06:00
"net/http"
"sync"
"time"
"nhooyr.io/websocket"
2024-01-27 13:24:42 -06:00
"nhooyr.io/websocket/wsjson"
2024-01-22 20:16:10 -06:00
2024-01-27 13:24:42 -06:00
"git.sunturtle.xyz/studio/shotgun/game"
2024-01-22 20:16:10 -06:00
"git.sunturtle.xyz/studio/shotgun/lobby"
"git.sunturtle.xyz/studio/shotgun/player"
"git.sunturtle.xyz/studio/shotgun/serve"
)
type Server struct {
2024-01-27 13:24:42 -06:00
l *lobby.Lobby
2024-01-22 20:16:10 -06:00
creds db
sessions db
2024-02-01 20:40:59 -06:00
2024-01-22 20:16:10 -06:00
mu sync.Mutex
2024-01-27 13:24:42 -06:00
pp map[player.ID]*websocket.Conn
2024-01-22 20:16:10 -06:00
}
type db interface {
2024-02-01 21:02:30 -06:00
player.RowQuerier
player.Execer
}
2024-01-22 20:16:10 -06:00
type person struct {
conn *websocket.Conn
id player.ID
}
2024-01-27 13:24:42 -06:00
func (s *Server) person(p player.ID) person {
2024-01-22 20:16:10 -06:00
s.mu.Lock()
defer s.mu.Unlock()
2024-01-27 13:24:42 -06:00
return person{conn: s.pp[p], id: p}
2024-01-22 20:16:10 -06:00
}
2024-01-27 13:24:42 -06:00
func (s *Server) accept(w http.ResponseWriter, r *http.Request, p player.ID) (person, error) {
conn, err := websocket.Accept(w, r, nil)
if err != nil {
return person{}, err
}
2024-01-27 13:51:20 -06:00
slog.Debug("upgraded", "player", p)
2024-01-22 20:16:10 -06:00
s.mu.Lock()
defer s.mu.Unlock()
2024-01-27 13:24:42 -06:00
s.pp[p] = conn
return person{conn: conn, id: p}, nil
2024-01-22 20:16:10 -06:00
}
2024-01-27 13:24:42 -06:00
func (s *Server) left(p player.ID) {
2024-01-22 20:16:10 -06:00
s.mu.Lock()
2024-01-27 13:24:42 -06:00
// NOTE(zeph): neither map index nor map delete can panic, so this critical
// section does not need a defer
c := s.pp[p]
delete(s.pp, p)
s.mu.Unlock()
if c != nil {
// We don't care about the error here since the connection is leaving.
// It's probably already closed anyway.
2024-01-27 13:51:20 -06:00
slog.Debug("leaving", "player", p)
2024-01-27 13:24:42 -06:00
c.Close(websocket.StatusNormalClosure, "bye")
}
2024-01-22 20:16:10 -06:00
}
2024-02-01 21:02:30 -06:00
func (s *Server) Register(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
slog := slog.With(
slog.String("remote", r.RemoteAddr),
slog.String("forwarded-for", r.Header.Get("X-Forwarded-For")),
slog.String("user-agent", r.UserAgent()),
)
if err := r.ParseForm(); err != nil {
slog.WarnContext(ctx, "error parsing form on register", "err", err.Error())
http.Error(w, "what", http.StatusBadRequest)
return
}
user, pass := r.PostFormValue("user"), r.PostFormValue("pass")
if user == "" || pass == "" {
slog.WarnContext(ctx, "missing user or pass on register")
http.Error(w, "missing credentials", http.StatusBadRequest)
return
}
err := player.Register(ctx, s.creds, user, pass)
if err != nil {
slog.ErrorContext(ctx, "registration failed", "err", err.Error())
http.Error(w, "something went wrong, maybe someone already has that username, idk", http.StatusInternalServerError)
return
}
2024-02-02 13:14:00 -06:00
p, err := player.Login(ctx, s.creds, user, pass)
if err != nil {
slog.ErrorContext(ctx, "login failed", "err", err.Error())
http.Error(w, "no", http.StatusUnauthorized)
return
}
id, err := player.StartSession(ctx, s.sessions, p, time.Now())
if err != nil {
slog.ErrorContext(ctx, "failed to create session", "player", p, "err", err.Error())
http.Error(w, "something went wrong", http.StatusInternalServerError)
return
}
serve.SetSession(w, id)
2024-02-01 21:02:30 -06:00
http.Redirect(w, r, "/", http.StatusSeeOther)
slog.InfoContext(ctx, "registered", "user", user)
}
2024-02-01 20:40:59 -06:00
func (s *Server) Login(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
slog := slog.With(
slog.String("remote", r.RemoteAddr),
slog.String("forwarded-for", r.Header.Get("X-Forwarded-For")),
slog.String("user-agent", r.UserAgent()),
)
if err := r.ParseForm(); err != nil {
slog.WarnContext(ctx, "error parsing form on login", "err", err.Error())
http.Error(w, "what", http.StatusBadRequest)
return
}
user, pass := r.PostFormValue("user"), r.PostFormValue("pass")
if user == "" || pass == "" {
slog.WarnContext(ctx, "missing user or pass on login")
http.Error(w, "missing credentials", http.StatusBadRequest)
return
}
p, err := player.Login(ctx, s.creds, user, pass)
if err != nil {
slog.ErrorContext(ctx, "login failed", "err", err.Error())
http.Error(w, "no", http.StatusUnauthorized)
return
}
id, err := player.StartSession(ctx, s.sessions, p, time.Now())
if err != nil {
slog.ErrorContext(ctx, "failed to create session", "player", p, "err", err.Error())
http.Error(w, "something went wrong", http.StatusInternalServerError)
return
}
serve.SetSession(w, id)
2024-02-01 21:02:30 -06:00
http.Redirect(w, r, "/", http.StatusSeeOther)
slog.InfoContext(ctx, "logged in", "player", p, "id", id)
2024-02-01 20:40:59 -06:00
}
2024-02-02 17:30:35 -06:00
func (s *Server) Logout(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
slog := slog.With(
slog.String("remote", r.RemoteAddr),
slog.String("forwarded-for", r.Header.Get("X-Forwarded-For")),
slog.String("user-agent", r.UserAgent()),
)
id := serve.ReqSession(ctx)
if id == (player.Session{}) {
slog.WarnContext(ctx, "no session on logout")
http.Error(w, "what", http.StatusUnauthorized)
panic("unreachable")
}
err := player.Logout(ctx, s.sessions, id)
if err != nil {
slog.ErrorContext(ctx, "logout failed", "err", err.Error())
http.Error(w, "something went wrong", http.StatusInternalServerError)
return
}
serve.RemoveSession(w)
slog.InfoContext(ctx, "logged out", "session", id)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (s *Server) Me(w http.ResponseWriter, r *http.Request) {
id := serve.ReqPlayer(r.Context())
if id == (player.ID{}) {
panic("missing player ID")
}
q := struct {
ID player.ID `json:"id"`
}{id}
json.NewEncoder(w).Encode(q)
}
2024-01-22 20:16:10 -06:00
// Queue connects players to games. The connection immediately upgrades.
// This handler MUST be wrapped in [serve.WithPlayerID].
func (s *Server) Queue(w http.ResponseWriter, r *http.Request) {
p := serve.ReqPlayer(r.Context())
2024-01-27 13:24:42 -06:00
person, err := s.accept(w, r, p)
2024-01-22 20:16:10 -06:00
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
2024-01-27 13:24:42 -06:00
s.joinAndServe(person)
2024-01-22 20:16:10 -06:00
}
func (s *Server) joinAndServe(p person) {
2024-01-27 13:51:20 -06:00
slog.Debug("joining", "player", p.id)
2024-01-22 20:16:10 -06:00
ctx, stop := context.WithTimeoutCause(context.Background(), 10*time.Minute, errQueueEmpty)
2024-02-02 21:18:03 -06:00
ch := make(chan struct{})
2024-02-02 20:48:45 -06:00
go func() {
2024-02-02 21:18:03 -06:00
defer close(ch)
2024-02-02 21:29:13 -06:00
_, _, err := p.conn.Read(ctx)
if err != nil {
slog.ErrorContext(ctx, "player dropped on hello", "player", p.id, "err", err.Error())
2024-02-02 20:48:45 -06:00
}
}()
2024-01-27 13:24:42 -06:00
id, chall, deal := s.l.Queue(ctx, p.id)
2024-01-22 20:16:10 -06:00
stop()
2024-02-02 21:18:03 -06:00
<-ch
2024-01-27 13:24:42 -06:00
if id == (lobby.GameID{}) {
2024-01-22 20:16:10 -06:00
// Context canceled.
2024-01-27 13:24:42 -06:00
p.conn.Close(websocket.StatusTryAgainLater, "sorry, queue is empty...")
return
}
if deal {
g := game.New(p.id, chall)
other := s.person(chall)
// TODO(zeph): save the game state s.t. we can provide a join channel
go gameActor(context.TODO(), g, p, other, nil)
2024-01-27 13:24:42 -06:00
}
// Reply with the game ID so they can share.
r := serve.GameStart{
ID: id,
Dealer: deal,
2024-01-27 13:24:42 -06:00
}
if err := wsjson.Write(context.TODO(), p.conn, r); err != nil {
2024-01-27 13:51:20 -06:00
slog.WarnContext(ctx, "got a game but player dropped", "game", id, "player", p.id)
2024-01-27 13:24:42 -06:00
s.left(p.id)
2024-01-22 20:16:10 -06:00
return
}
}
var errQueueEmpty = errors.New("sorry, queue is empty")