Move server logic into server file

This commit is contained in:
Neale Pickett 2020-03-01 17:10:55 -06:00
parent fbba5ac004
commit 3eafa7a328
7 changed files with 265 additions and 188 deletions

View File

@ -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()
}

View File

@ -9,42 +9,34 @@ import (
type HTTPServer struct { type HTTPServer struct {
*http.ServeMux *http.ServeMux
Puzzles PuzzleProvider server *MothServer
Theme ThemeProvider base string
State StateProvider
Base string
} }
func NewHTTPServer(base string, theme ThemeProvider, state StateProvider, puzzles PuzzleProvider) *HTTPServer { func NewHTTPServer(base string, server *MothServer) *HTTPServer {
base = strings.TrimRight(base, "/") base = strings.TrimRight(base, "/")
h := &HTTPServer{ h := &HTTPServer{
ServeMux: http.NewServeMux(), ServeMux: http.NewServeMux(),
Puzzles: puzzles, server: server,
Theme: theme, base: base,
State: state,
Base: base,
} }
h.HandleFunc(base+"/", h.ThemeHandler) h.HandleMothFunc("/", h.ThemeHandler)
h.HandleFunc(base+"/state", h.StateHandler) h.HandleMothFunc("/state", h.StateHandler)
h.HandleFunc(base+"/register", h.RegisterHandler) h.HandleMothFunc("/register", h.RegisterHandler)
h.HandleFunc(base+"/answer", h.AnswerHandler) h.HandleMothFunc("/answer", h.AnswerHandler)
h.HandleFunc(base+"/content/", h.ContentHandler) h.HandleMothFunc("/content/", h.ContentHandler)
return h return h
} }
func (h *HTTPServer) Run(bindStr string) { func (h *HTTPServer) HandleMothFunc(
log.Printf("Listening on %s", bindStr) pattern string,
log.Fatal(http.ListenAndServe(bindStr, h)) 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)
type MothResponseWriter struct {
statusCode *int
http.ResponseWriter
}
func (w MothResponseWriter) WriteHeader(statusCode int) {
*w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
} }
// This gives Instances the signature of http.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 path := req.URL.Path
if path == "/" { if path == "/" {
path = "/index.html" path = "/index.html"
} }
f, err := h.Theme.Open(path) f, mtime, err := mh.ThemeOpen(path)
if err != nil { if err != nil {
http.NotFound(w, req) http.NotFound(w, req)
return return
} }
defer f.Close() defer f.Close()
mtime, _ := h.Theme.ModTime(path)
http.ServeContent(w, req, path, mtime, f) http.ServeContent(w, req, path, mtime, f)
} }
func (h *HTTPServer) StateHandler(w http.ResponseWriter, req *http.Request) { func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
var state struct { JSONWrite(w, mh.ExportState())
Config struct {
Devel bool
}
Messages string
TeamNames map[string]string
PointsLog []Award
Puzzles map[string][]int
} }
teamId := req.FormValue("id") func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
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) RegisterHandler(w http.ResponseWriter, req *http.Request) {
teamId := req.FormValue("id")
teamName := req.FormValue("name") 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()) JSendf(w, JSendFail, "not registered", err.Error())
} else { } else {
JSendf(w, JSendSuccess, "registered", "Team ID registered") JSendf(w, JSendSuccess, "registered", "Team ID registered")
} }
} }
func (h *HTTPServer) AnswerHandler(w http.ResponseWriter, req *http.Request) { func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
JSendf(w, JSendFail, "unimplemented", "I haven't written this yet") 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) { func (h *HTTPServer) ContentHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
teamId := req.FormValue("id") trimLen := len(h.base) + len("/content/")
if _, err := h.State.TeamName(teamId); err != nil {
http.Error(w, "Team Not Found", http.StatusNotFound)
return
}
trimLen := len(h.Base) + len("/content/")
parts := strings.SplitN(req.URL.Path[trimLen:], "/", 3) parts := strings.SplitN(req.URL.Path[trimLen:], "/", 3)
if len(parts) < 3 { if len(parts) < 3 {
http.Error(w, "Not Found", http.StatusNotFound) 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) points, _ := strconv.Atoi(pointsStr)
mf, err := h.Puzzles.Open(cat, points, filename) mf, mtime, err := mh.PuzzlesOpen(cat, points, filename)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusNotFound) http.Error(w, err.Error(), http.StatusNotFound)
return return
} }
defer mf.Close() defer mf.Close()
mt, _ := h.Puzzles.ModTime(cat, points, filename) http.ServeContent(w, req, filename, mtime, mf)
http.ServeContent(w, req, filename, mt, mf)
} }

View File

@ -68,6 +68,7 @@ func main() {
go custodian(*refreshInterval, []Component{theme, state, puzzles}) 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) httpd.Run(*bindStr)
} }

View File

@ -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] mb, ok := m.categories[cat]
if ! ok { 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) f, err := mb.Open(fmt.Sprintf("content/%d/%s", points, filename))
return mb.Open(path) return f, mb.ModTime(), err
}
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
} }
func (m *Mothballs) Inventory() []Category { func (m *Mothballs) Inventory() []Category {
@ -64,6 +55,29 @@ func (m *Mothballs) Inventory() []Category {
return categories 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() { func (m *Mothballs) Update() {
// Any new categories? // Any new categories?
files, err := afero.ReadDir(m.Fs, "/") files, err := afero.ReadDir(m.Fs, "/")

164
cmd/mothd/server.go Normal file
View File

@ -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
}

View File

@ -8,7 +8,6 @@ import (
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
) )
@ -146,36 +145,10 @@ func (s *State) PointsLog() []*Award {
return pointsLog return pointsLog
} }
// Return an exportable points log for a client // Retrieve current messages
func (s *State) Export(teamId string) *StateExport { func (s *State) Messages() string {
var export StateExport
bMessages, _ := afero.ReadFile(s, "messages.html") bMessages, _ := afero.ReadFile(s, "messages.html")
export.Messages = string(bMessages) return 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
} }
// AwardPoints gives points to teamId in category. // AwardPoints gives points to teamId in category.

View File

@ -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. // I don't understand why I need this. The type checking system is weird here.
func (t *Theme) Open(name string) (ReadSeekCloser, error) { func (t *Theme) Open(name string) (ReadSeekCloser, time.Time, error) {
return t.Fs.Open(name) f, err := t.Fs.Open(name)
if err != nil {
return nil, time.Time{}, err
} }
func (t *Theme) ModTime(name string) (mt time.Time, err error) { fi, err := f.Stat()
fi, err := t.Fs.Stat(name) if err != nil {
if err == nil { f.Close()
mt = fi.ModTime() return nil, time.Time{}, err
} }
return
return f, fi.ModTime(), nil
} }
func (t *Theme) Update() { func (t *Theme) Update() {