From 3eafa7a32868b02b0fd8064b7bd81c35c7fd9e58 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 1 Mar 2020 17:10:55 -0600 Subject: [PATCH] Move server logic into server file --- cmd/mothd/common.go | 44 ----------- cmd/mothd/httpd.go | 148 ++++++++++++++----------------------- cmd/mothd/main.go | 3 +- cmd/mothd/mothballs.go | 40 ++++++---- cmd/mothd/server.go | 164 +++++++++++++++++++++++++++++++++++++++++ cmd/mothd/state.go | 33 +-------- cmd/mothd/theme.go | 21 +++--- 7 files changed, 265 insertions(+), 188 deletions(-) delete mode 100644 cmd/mothd/common.go create mode 100644 cmd/mothd/server.go diff --git a/cmd/mothd/common.go b/cmd/mothd/common.go deleted file mode 100644 index f773d3c..0000000 --- a/cmd/mothd/common.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "io" - "time" -) - -type Category struct { - Name string - Puzzles []int -} - -type ReadSeekCloser interface { - io.Reader - io.Seeker - io.Closer -} - -type PuzzleProvider interface { - Open(cat string, points int, path string) (ReadSeekCloser, error) - ModTime(cat string, points int, path string) (time.Time, error) - Inventory() []Category -} - -type ThemeProvider interface { - Open(path string) (ReadSeekCloser, error) - ModTime(path string) (time.Time, error) -} - -type StateExport struct { - Messages string - TeamNames map[string]string - PointsLog []Award -} - -type StateProvider interface { - Export(teamId string) *StateExport - TeamName(teamId string) (string, error) - SetTeamName(teamId, teamName string) error -} - -type Component interface { - Update() -} diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index 50c6af7..f7d2137 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -9,42 +9,34 @@ import ( type HTTPServer struct { *http.ServeMux - Puzzles PuzzleProvider - Theme ThemeProvider - State StateProvider - Base string + server *MothServer + base string } -func NewHTTPServer(base string, theme ThemeProvider, state StateProvider, puzzles PuzzleProvider) *HTTPServer { +func NewHTTPServer(base string, server *MothServer) *HTTPServer { base = strings.TrimRight(base, "/") h := &HTTPServer{ ServeMux: http.NewServeMux(), - Puzzles: puzzles, - Theme: theme, - State: state, - Base: base, + server: server, + base: base, } - h.HandleFunc(base+"/", h.ThemeHandler) - h.HandleFunc(base+"/state", h.StateHandler) - h.HandleFunc(base+"/register", h.RegisterHandler) - h.HandleFunc(base+"/answer", h.AnswerHandler) - h.HandleFunc(base+"/content/", h.ContentHandler) + h.HandleMothFunc("/", h.ThemeHandler) + h.HandleMothFunc("/state", h.StateHandler) + h.HandleMothFunc("/register", h.RegisterHandler) + h.HandleMothFunc("/answer", h.AnswerHandler) + h.HandleMothFunc("/content/", h.ContentHandler) return h } -func (h *HTTPServer) Run(bindStr string) { - log.Printf("Listening on %s", bindStr) - log.Fatal(http.ListenAndServe(bindStr, h)) -} - -type MothResponseWriter struct { - statusCode *int - http.ResponseWriter -} - -func (w MothResponseWriter) WriteHeader(statusCode int) { - *w.statusCode = statusCode - w.ResponseWriter.WriteHeader(statusCode) +func (h *HTTPServer) HandleMothFunc( + pattern string, + mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request), +) { + handler := func(w http.ResponseWriter, req *http.Request) { + mh := h.server.NewHandler(req.FormValue("id")) + mothHandler(mh, w, req) + } + h.HandleFunc(h.base + pattern, handler) } // This gives Instances the signature of http.Handler @@ -63,90 +55,65 @@ func (h *HTTPServer) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) { ) } -func (h *HTTPServer) ThemeHandler(w http.ResponseWriter, req *http.Request) { +type MothResponseWriter struct { + statusCode *int + http.ResponseWriter +} + +func (w MothResponseWriter) WriteHeader(statusCode int) { + *w.statusCode = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} + +func (h *HTTPServer) Run(bindStr string) { + log.Printf("Listening on %s", bindStr) + log.Fatal(http.ListenAndServe(bindStr, h)) +} + +func (h *HTTPServer) ThemeHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { path := req.URL.Path if path == "/" { path = "/index.html" } - - f, err := h.Theme.Open(path) + + f, mtime, err := mh.ThemeOpen(path) if err != nil { http.NotFound(w, req) return } defer f.Close() - mtime, _ := h.Theme.ModTime(path) http.ServeContent(w, req, path, mtime, f) } -func (h *HTTPServer) StateHandler(w http.ResponseWriter, req *http.Request) { - var state struct { - Config struct { - Devel bool - } - Messages string - TeamNames map[string]string - PointsLog []Award - Puzzles map[string][]int - } - - teamId := req.FormValue("id") - export := h.State.Export(teamId) - - state.Messages = export.Messages - state.TeamNames = export.TeamNames - state.PointsLog = export.PointsLog - - state.Puzzles = make(map[string][]int) - - //XXX: move to brains.go - for _, category := range h.Puzzles.Inventory() { - maxSolved := 0 - - // XXX: We don't have to iterate the log for every category - for _, a := range export.PointsLog { - if (a.Category == category.Name) && (a.Points > maxSolved) { - maxSolved = a.Points - } - } - - // Append sentry (end of puzzles) - allPuzzles := append(category.Puzzles, 0) - puzzles := make([]int, 0, len(allPuzzles)) - for i, val := range allPuzzles { - puzzles = allPuzzles[:i+1] - if val > maxSolved { - break - } - } - state.Puzzles[category.Name] = puzzles - } - - JSONWrite(w, state) +func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { + JSONWrite(w, mh.ExportState()) } -func (h *HTTPServer) RegisterHandler(w http.ResponseWriter, req *http.Request) { - teamId := req.FormValue("id") +func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { teamName := req.FormValue("name") - if err := h.State.SetTeamName(teamId, teamName); err != nil { + if err := mh.Register(teamName); err != nil { JSendf(w, JSendFail, "not registered", err.Error()) } else { JSendf(w, JSendSuccess, "registered", "Team ID registered") } } -func (h *HTTPServer) AnswerHandler(w http.ResponseWriter, req *http.Request) { - JSendf(w, JSendFail, "unimplemented", "I haven't written this yet") +func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { + cat := req.FormValue("cat") + pointstr := req.FormValue("points") + answer := req.FormValue("answer") + + points, _ := strconv.Atoi(pointstr) + + if err := mh.CheckAnswer(cat, points, answer); err != nil { + JSendf(w, JSendFail, "not accepted", err.Error()) + } else { + JSendf(w, JSendSuccess, "accepted", "%d points awarded in %s", points, cat) + } } -func (h *HTTPServer) ContentHandler(w http.ResponseWriter, req *http.Request) { - teamId := req.FormValue("id") - if _, err := h.State.TeamName(teamId); err != nil { - http.Error(w, "Team Not Found", http.StatusNotFound) - return - } - - trimLen := len(h.Base) + len("/content/") +func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { + trimLen := len(h.base) + len("/content/") parts := strings.SplitN(req.URL.Path[trimLen:], "/", 3) if len(parts) < 3 { http.Error(w, "Not Found", http.StatusNotFound) @@ -163,13 +130,12 @@ func (h *HTTPServer) ContentHandler(w http.ResponseWriter, req *http.Request) { points, _ := strconv.Atoi(pointsStr) - mf, err := h.Puzzles.Open(cat, points, filename) + mf, mtime, err := mh.PuzzlesOpen(cat, points, filename) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } defer mf.Close() - mt, _ := h.Puzzles.ModTime(cat, points, filename) - http.ServeContent(w, req, filename, mt, mf) + http.ServeContent(w, req, filename, mtime, mf) } diff --git a/cmd/mothd/main.go b/cmd/mothd/main.go index 413ad77..c720bff 100644 --- a/cmd/mothd/main.go +++ b/cmd/mothd/main.go @@ -68,6 +68,7 @@ func main() { go custodian(*refreshInterval, []Component{theme, state, puzzles}) - httpd := NewHTTPServer(*base, theme, state, puzzles) + server := NewMothServer(puzzles, theme, state) + httpd := NewHTTPServer(*base, server) httpd.Run(*bindStr) } diff --git a/cmd/mothd/mothballs.go b/cmd/mothd/mothballs.go index b57a537..bca7e42 100644 --- a/cmd/mothd/mothballs.go +++ b/cmd/mothd/mothballs.go @@ -22,23 +22,14 @@ func NewMothballs(fs afero.Fs) *Mothballs { } } -func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, error) { +func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) { mb, ok := m.categories[cat] if ! ok { - return nil, fmt.Errorf("No such category: %s", cat) + return nil, time.Time{}, fmt.Errorf("No such category: %s", cat) } - path := fmt.Sprintf("content/%d/%s", points, filename) - return mb.Open(path) -} - -func (m *Mothballs) ModTime(cat string, points int, filename string) (mt time.Time, err error) { - mb, ok := m.categories[cat] - if ! ok { - return mt, fmt.Errorf("No such category: %s", cat) - } - mt = mb.ModTime() - return + f, err := mb.Open(fmt.Sprintf("content/%d/%s", points, filename)) + return f, mb.ModTime(), err } func (m *Mothballs) Inventory() []Category { @@ -64,6 +55,29 @@ func (m *Mothballs) Inventory() []Category { return categories } +func (m *Mothballs) CheckAnswer(cat string, points int, answer string) error { + zfs, ok := m.categories[cat] + if ! ok { + return fmt.Errorf("No such category: %s", cat) + } + + af, err := zfs.Open("answers.txt") + if err != nil { + return fmt.Errorf("No answers.txt file") + } + defer af.Close() + + needle := fmt.Sprintf("%d %s", points, answer) + scanner := bufio.NewScanner(af) + for scanner.Scan() { + if scanner.Text() == needle { + return nil + } + } + + return fmt.Errorf("Invalid answer") +} + func (m *Mothballs) Update() { // Any new categories? files, err := afero.ReadDir(m.Fs, "/") diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go new file mode 100644 index 0000000..061f45a --- /dev/null +++ b/cmd/mothd/server.go @@ -0,0 +1,164 @@ +package main + +import ( + "io" + "time" + "fmt" + "strconv" +) + +type Category struct { + Name string + Puzzles []int +} + +type ReadSeekCloser interface { + io.Reader + io.Seeker + io.Closer +} + +type StateExport struct { + Config struct { + Devel bool + } + Messages string + TeamNames map[string]string + PointsLog []Award + Puzzles map[string][]int +} + +type PuzzleProvider interface { + Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error) + Inventory() []Category + CheckAnswer(cat string, points int, answer string) error + Component +} + +type ThemeProvider interface { + Open(path string) (ReadSeekCloser, time.Time, error) + Component +} + +type StateProvider interface { + Messages() string + PointsLog() []*Award + TeamName(teamId string) (string, error) + SetTeamName(teamId, teamName string) error + Component +} + + +type Component interface { + Update() +} + + +type MothServer struct { + Puzzles PuzzleProvider + Theme ThemeProvider + State StateProvider +} + +func NewMothServer(puzzles PuzzleProvider, theme ThemeProvider, state StateProvider) *MothServer { + return &MothServer{ + Puzzles: puzzles, + Theme: theme, + State: state, + } +} + +func (s *MothServer) NewHandler(teamId string) MothRequestHandler { + return MothRequestHandler{ + MothServer: s, + teamId: teamId, + } +} + +// XXX: Come up with a better name for this. +type MothRequestHandler struct { + *MothServer + teamId string +} + +func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (ReadSeekCloser, time.Time, error) { + // XXX: Make sure this puzzle is unlocked + return mh.Puzzles.Open(cat, points, path) +} + +func (mh *MothRequestHandler) ThemeOpen(path string) (ReadSeekCloser, time.Time, error) { + return mh.Theme.Open(path) +} + +func (mh *MothRequestHandler) xxxTeamName() string { + teamName, _ := mh.State.TeamName(mh.teamId) + return teamName +} + +func (mh *MothRequestHandler) Register(teamName string) error { + // XXX: Should we just return success if the team is already registered? + // XXX: Should this function be renamed to Login? + if teamName == "" { + return fmt.Errorf("Empty team name") + } + return mh.State.SetTeamName(mh.teamId, teamName) +} + +func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string) error { + return mh.Puzzles.CheckAnswer(cat, points, answer) +} + +func (mh *MothRequestHandler) ExportState() *StateExport { + export := StateExport{} + + teamName, _ := mh.State.TeamName(mh.teamId) + + export.Messages = mh.State.Messages() + export.TeamNames = map[string]string{"self": teamName} + + // Anonymize team IDs in points log, and write out team names + pointsLog := mh.State.PointsLog() + exportIds := map[string]string{mh.teamId: "self"} + maxSolved := map[string]int{} + export.PointsLog = make([]Award, len(pointsLog)) + for logno, award := range pointsLog { + exportAward := *award + if id, ok := exportIds[award.TeamId]; ok { + exportAward.TeamId = id + } else { + exportId := strconv.Itoa(logno) + name, _ := mh.State.TeamName(award.TeamId) + exportAward.TeamId = exportId + exportIds[award.TeamId] = exportAward.TeamId + export.TeamNames[exportId] = name + } + export.PointsLog[logno] = exportAward + + // Record the highest-value unlocked puzzle in each category + if award.Points > maxSolved[award.Category] { + maxSolved[award.Category] = award.Points + } + } + + + export.Puzzles = make(map[string][]int) + + //XXX: move to brains.go + for _, category := range mh.Puzzles.Inventory() { + // Append sentry (end of puzzles) + allPuzzles := append(category.Puzzles, 0) + + max := maxSolved[category.Name] + + puzzles := make([]int, 0, len(allPuzzles)) + for i, val := range allPuzzles { + puzzles = allPuzzles[:i+1] + if val > max { + break + } + } + export.Puzzles[category.Name] = puzzles + } + + return &export +} \ No newline at end of file diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index ff3db54..7055ba0 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -8,7 +8,6 @@ import ( "math/rand" "os" "path/filepath" - "strconv" "strings" "time" ) @@ -146,36 +145,10 @@ func (s *State) PointsLog() []*Award { return pointsLog } -// Return an exportable points log for a client -func (s *State) Export(teamId string) *StateExport { - var export StateExport - +// Retrieve current messages +func (s *State) Messages() string { bMessages, _ := afero.ReadFile(s, "messages.html") - export.Messages = string(bMessages) - - teamName, _ := s.TeamName(teamId) - export.TeamNames = map[string]string{"self": teamName} - - pointsLog := s.PointsLog() - export.PointsLog = make([]Award, len(pointsLog)) - - // Read in points - exportIds := map[string]string{teamId: "self"} - for logno, award := range pointsLog { - exportAward := *award - if id, ok := exportIds[award.TeamId]; ok { - exportAward.TeamId = id - } else { - exportId := strconv.Itoa(logno) - name, _ := s.TeamName(award.TeamId) - exportAward.TeamId = exportId - exportIds[award.TeamId] = exportAward.TeamId - export.TeamNames[exportId] = name - } - export.PointsLog[logno] = exportAward - } - - return &export + return string(bMessages) } // AwardPoints gives points to teamId in category. diff --git a/cmd/mothd/theme.go b/cmd/mothd/theme.go index afb7e7e..e020fe7 100644 --- a/cmd/mothd/theme.go +++ b/cmd/mothd/theme.go @@ -16,16 +16,19 @@ func NewTheme(fs afero.Fs) *Theme { } // I don't understand why I need this. The type checking system is weird here. -func (t *Theme) Open(name string) (ReadSeekCloser, error) { - return t.Fs.Open(name) -} - -func (t *Theme) ModTime(name string) (mt time.Time, err error) { - fi, err := t.Fs.Stat(name) - if err == nil { - mt = fi.ModTime() +func (t *Theme) Open(name string) (ReadSeekCloser, time.Time, error) { + f, err := t.Fs.Open(name) + if err != nil { + return nil, time.Time{}, err } - return + + fi, err := f.Stat() + if err != nil { + f.Close() + return nil, time.Time{}, err + } + + return f, fi.ModTime(), nil } func (t *Theme) Update() {