mirror of https://github.com/dirtbags/moth.git
Move server logic into server file
This commit is contained in:
parent
fbba5ac004
commit
3eafa7a328
|
@ -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()
|
||||
}
|
|
@ -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))
|
||||
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)
|
||||
}
|
||||
|
||||
type MothResponseWriter struct {
|
||||
statusCode *int
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (w MothResponseWriter) WriteHeader(statusCode int) {
|
||||
*w.statusCode = statusCode
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
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
|
||||
func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||
JSONWrite(w, mh.ExportState())
|
||||
}
|
||||
|
||||
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) 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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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, "/")
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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) Open(name string) (ReadSeekCloser, time.Time, error) {
|
||||
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 := t.Fs.Stat(name)
|
||||
if err == nil {
|
||||
mt = fi.ModTime()
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
return
|
||||
|
||||
return f, fi.ModTime(), nil
|
||||
}
|
||||
|
||||
func (t *Theme) Update() {
|
||||
|
|
Loading…
Reference in New Issue