mayhem-party/src/device/input/parse/v01/server.go

378 lines
8.8 KiB
Go

package v01
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math/rand"
"mayhem-party/src/device/input/wrap"
"net/http"
"os"
"strings"
"sync"
"syscall"
"time"
"gopkg.in/yaml.v2"
)
func (v01 *V01) listen() {
if v01.cfg.Feedback.Addr == "" {
return
}
v01._listen()
}
func (v01 *V01) _listen() {
mutex := &sync.Mutex{}
s := &http.Server{
Addr: v01.cfg.Feedback.Addr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() { log.Printf("%vms | %s %s", time.Since(start).Milliseconds(), r.Method, r.URL) }()
v01.cfg.lock.Lock()
defer v01.cfg.lock.Unlock()
if r.Method == http.MethodGet {
mutex.Lock()
defer mutex.Unlock()
} else {
mutex.Lock()
defer mutex.Unlock()
}
v01.ServeHTTP(w, r)
v01.stashConfig() // TODO
}),
}
go func() {
<-v01.ctx.Done()
log.Println("closing v01 server")
s.Close()
}()
log.Println("starting v01 server")
if err := s.ListenAndServe(); err != nil && v01.ctx.Err() == nil {
log.Println("err with v01 server", err)
panic(err)
}
}
func (v01 *V01) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(v01.ctx)
v01.serveHTTP(w, r)
v01.serveGlobalQueries(r)
}
func (v01 *V01) serveHTTP(w http.ResponseWriter, r *http.Request) {
switch strings.Split(r.URL.Path[1:], "/")[0] {
case "":
v01.getUserFeedback(w, r)
case "broadcast":
v01.servePutBroadcast(w, r)
case "config":
v01.serveConfig(w, r)
case "gm":
v01.serveGM(w, r)
}
}
func (v01 *V01) getUserFeedback(w http.ResponseWriter, r *http.Request) {
user := v01.cfg.Users[r.URL.Query().Get("user")]
msg := user.State.Message
if msg == "" {
msg = v01.cfg.Broadcast.Message
}
alias := user.State.GM.Alias
if alias == "" {
alias = user.State.GM.LastAlias
}
if alias != "" {
msg = fmt.Sprintf("%s (Your secret word is '%s'. Make **someone else** say it!)", msg, alias)
}
w.Write([]byte(msg + "\n\n"))
v01.serveGMStatus(w)
if v01.cfg.Quiet {
w.Write([]byte("\n\n"))
v01.serveGMVoteRead(w)
}
}
func (v01 *V01) servePutBroadcast(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
v01.servePutBroadcastValue(string(b))
}
func (v01 *V01) servePutBroadcastValue(v string) {
v01.cfg.Broadcast.Message = v
}
func (v01 *V01) serveConfig(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
v01.serveGetConfig(w, r)
} else {
v01.servePatchConfig(w, r)
}
}
func (v01 *V01) serveGetConfig(w http.ResponseWriter, r *http.Request) {
b, _ := json.Marshal(v01.cfg)
w.Write(b)
}
func (v01 *V01) servePatchConfig(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var v []interface{}
if err := json.Unmarshal(b, &v); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
v01.cfg = v01.cfg.WithPatch(v)
if err := v01.stashConfig(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (v01 *V01) stashConfig() error {
if b, err := yaml.Marshal(v01.cfg); err == nil && FlagParseV01Config != "" {
if err := os.WriteFile(FlagParseV01Config, b, os.ModePerm); err != nil {
return err
}
} else if err != nil {
return err
}
return nil
}
func (v01 *V01) serveGlobalQueries(r *http.Request) {
v01.serveGlobalQuerySay(r)
v01.serveGlobalQueryRefresh(r)
}
func (v01 *V01) serveGlobalQuerySay(r *http.Request) {
text := r.URL.Query().Get("say")
if text == "" {
text = r.Header.Get("say")
}
if text == "" {
return
}
go v01.tts(text)
}
func (v01 *V01) serveGlobalQueryRefresh(r *http.Request) {
if _, ok := r.URL.Query()["refresh"]; !ok {
return
}
select {
case wrap.ChSigUsr1 <- syscall.SIGUSR1:
default:
}
}
func (v01 *V01) serveGM(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/gm/rpc/status":
v01.serveGMStatus(w)
case "/gm/rpc/broadcastSomeoneSaidAlias":
v01.serveGMSomeoneSaidAlias(w, r)
case "/gm/rpc/fillNonPlayerAliases":
v01.serveGMFillNonPlayerAliases(w, r)
case "/gm/rpc/vote":
v01.serveGMVote(w, r)
case "/gm/rpc/elect":
v01.serveGMElect(w, r)
case "/gm/rpc/shuffle":
v01.serveGMShuffle(r)
case "/gm/rpc/swap":
if errCode, err := v01.serveGMSwap(r.URL.Query().Get("a"), r.URL.Query().Get("b")); err != nil {
http.Error(w, err.Error(), errCode)
return
}
default:
http.NotFound(w, r)
return
}
}
func (v01 *V01) serveGMStatus(w io.Writer) {
users := map[string]struct {
Lag time.Duration `yaml:"lag,omitempty"`
Player int `yaml:"player,omitempty"`
IdleFor time.Duration `yaml:"idle_for,omitempty"`
}{}
for k, v := range v01.cfg.Users {
v2 := users[k]
v2.Lag = time.Duration(v.Meta.LastLag) * time.Millisecond
v2.Player = v.State.Player
if v.Meta.LastTSMS > 0 {
v2.IdleFor = time.Since(time.Unix(0, v.Meta.LastTSMS*int64(time.Millisecond)))
}
users[k] = v2
}
yaml.NewEncoder(w).Encode(map[string]interface{}{
"Players": len(v01.cfg.Players),
"Users": users,
})
}
func (v01 *V01) serveGMSomeoneSaidAlias(w http.ResponseWriter, r *http.Request) {
v01.cfg.Quiet = true
for k, v := range v01.cfg.Users {
v.State.GM.LastAlias = v.State.GM.Alias
v.State.GM.Alias = ""
v01.cfg.Users[k] = v
}
v01.servePutBroadcastValue(fmt.Sprintf("<<SOMEONE SAID %q>>", strings.ToUpper(r.URL.Query().Get("message"))))
}
func (v01 *V01) serveGMFillNonPlayerAliases(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
var pool []string
yaml.Unmarshal(b, &pool)
n := 0
for _, v := range v01.cfg.Users {
if v.State.Player == 0 {
n += 1
}
}
if n < 1 {
w.WriteHeader(http.StatusNoContent)
return
}
if len(pool) < n {
http.Error(w, fmt.Sprintf("request body must contain a list of %v options", n), http.StatusBadRequest)
return
}
for i := 0; i < 100; i++ {
a, b := rand.Int()%len(pool), rand.Int()%len(pool)
pool[a], pool[b] = pool[b], pool[a]
}
i := 0
for k, v := range v01.cfg.Users {
if v.State.Player == 0 {
v.State.GM.Alias = pool[i]
v01.cfg.Users[k] = v
i += 1
}
}
}
func (v01 *V01) serveGMElect(w http.ResponseWriter, r *http.Request) {
alias := r.URL.Query().Get("alias")
aliasWinner := ""
votes := map[string]int{}
for k, v := range v01.cfg.Users {
votes[v.State.GM.Vote] = votes[v.State.GM.Vote] + 1
if v.State.GM.LastAlias == alias {
aliasWinner = k
}
}
if aliasWinner == "" {
http.Error(w, "who is "+alias+"?", http.StatusBadRequest)
return
}
threshold := 0.1 + float64(len(votes))/2.0
winner := ""
for k, v := range votes {
if float64(v) > threshold {
winner = k
}
}
if winner == "" {
v01.serveGMShuffle(r)
} else if _, err := v01.serveGMSwap(winner, aliasWinner); err != nil {
v01.serveGMShuffle(r)
}
for k, v := range v01.cfg.Users {
v.State.GM.Vote = ""
v01.cfg.Users[k] = v
}
yaml.NewEncoder(w).Encode(votes)
}
func (v01 *V01) serveGMVote(w http.ResponseWriter, r *http.Request) {
switch r.URL.Query().Get("payload") {
case "":
v01.serveGMVoteRead(w)
default:
v01.serveGMVoteWrite(w, r)
}
}
func (v01 *V01) serveGMVoteRead(w io.Writer) {
counts := map[string]string{}
for k, v := range v01.cfg.Users {
if v.State.GM.Vote != "" {
counts[k] = "voted"
} else {
counts[k] = "voting"
}
}
yaml.NewEncoder(w).Encode(counts)
}
func (v01 *V01) serveGMVoteWrite(w http.ResponseWriter, r *http.Request) {
voter := r.URL.Query().Get("user")
candidate := r.URL.Query().Get("payload")
v, ok := v01.cfg.Users[voter]
if _, ok2 := v01.cfg.Users[candidate]; !ok || !ok2 {
http.Error(w, "bad voter/candidate", http.StatusBadRequest)
return
}
v.State.GM.Vote = candidate
v01.cfg.Users[voter] = v
}
func (v01 *V01) serveGMShuffle(r *http.Request) {
poolSize := len(v01.cfg.Users)
if altSize := len(v01.cfg.Players); altSize > poolSize {
poolSize = altSize
}
pool := make([]int, poolSize)
if poolSize > 0 {
for i := range v01.cfg.Players {
pool[i] = i + 1
}
for i := 0; i < 30; i++ {
l := rand.Int() % poolSize
r := rand.Int() % poolSize
pool[l], pool[r] = pool[r], pool[l]
}
}
i := 0
msg := []string{}
for k, v := range v01.cfg.Users {
v.State.Player = pool[i]
v01.cfg.Users[k] = v
if pool[i] > 0 {
msg = append(msg, fmt.Sprintf("%s is now player %v", k, v.State.Player))
}
i += 1
}
v01.servePutBroadcastValue(strings.Join(msg, ", "))
v01.cfg.Quiet = false
}
func (v01 *V01) serveGMSwap(userA, userB string) (int, error) {
if userA == userB {
return http.StatusConflict, errors.New("/spiderman-pointing")
}
_, okA := v01.cfg.Users[userA]
_, okB := v01.cfg.Users[userB]
if !okA || !okB {
return http.StatusBadRequest, errors.New("who dat?")
}
a := v01.cfg.Users[userA]
b := v01.cfg.Users[userB]
a.State.Player, b.State.Player = b.State.Player, a.State.Player
v01.cfg.Users[userA] = a
v01.cfg.Users[userB] = b
v01.cfg.Quiet = false
v01.servePutBroadcastValue(fmt.Sprintf(`%s is now player %v and %s is now player %v`, userA, a.State.Player, userB, b.State.Player))
return http.StatusOK, nil
}