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("<>", 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 }