moth/cmd/mothd/server.go

204 lines
5.6 KiB
Go
Raw Normal View History

2020-03-01 16:10:55 -07:00
package main
import (
"fmt"
2020-08-14 20:26:04 -06:00
"io"
2020-03-01 16:10:55 -07:00
"strconv"
2020-08-14 20:26:04 -06:00
"time"
2020-08-17 17:43:57 -06:00
"github.com/dirtbags/moth/pkg/award"
2020-03-01 16:10:55 -07:00
)
2020-08-17 17:43:57 -06:00
// Category represents a puzzle category.
2020-03-01 16:10:55 -07:00
type Category struct {
Name string
Puzzles []int
}
2020-08-17 17:43:57 -06:00
// ReadSeekCloser defines a struct that can read, seek, and close.
2020-03-01 16:10:55 -07:00
type ReadSeekCloser interface {
io.Reader
io.Seeker
io.Closer
}
2020-08-17 17:43:57 -06:00
// StateExport is given to clients requesting the current state.
2020-03-01 16:10:55 -07:00
type StateExport struct {
Config struct {
Devel bool
}
Messages string
TeamNames map[string]string
2020-08-17 17:43:57 -06:00
PointsLog award.List
2020-08-14 20:26:04 -06:00
Puzzles map[string][]int
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
// PuzzleProvider defines what's required to provide puzzles.
2020-03-01 16:10:55 -07:00
type PuzzleProvider interface {
Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error)
Inventory() []Category
CheckAnswer(cat string, points int, answer string) error
2020-08-18 17:04:23 -06:00
Maintainer
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
// ThemeProvider defines what's required to provide a theme.
2020-03-01 16:10:55 -07:00
type ThemeProvider interface {
Open(path string) (ReadSeekCloser, time.Time, error)
2020-08-18 17:04:23 -06:00
Maintainer
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
// StateProvider defines what's required to provide MOTH state.
2020-03-01 16:10:55 -07:00
type StateProvider interface {
Messages() string
2020-08-17 17:43:57 -06:00
PointsLog() award.List
TeamName(teamID string) (string, error)
SetTeamName(teamID, teamName string) error
AwardPoints(teamID string, cat string, points int) error
2020-08-19 18:01:21 -06:00
LogEvent(msg string)
2020-08-18 17:04:23 -06:00
Maintainer
2020-03-01 16:10:55 -07:00
}
2020-08-18 17:04:23 -06:00
// Maintainer is something that can be maintained.
type Maintainer interface {
// Maintain is the maintenance loop.
// It will only be called once, when execution begins.
// It's okay to just exit if there's no maintenance to be done.
Maintain(updateInterval time.Duration)
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
// MothServer gathers together the providers that make up a MOTH server.
2020-03-01 16:10:55 -07:00
type MothServer struct {
Puzzles PuzzleProvider
2020-08-14 20:26:04 -06:00
Theme ThemeProvider
State StateProvider
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
// NewMothServer returns a new MothServer.
2020-03-01 16:10:55 -07:00
func NewMothServer(puzzles PuzzleProvider, theme ThemeProvider, state StateProvider) *MothServer {
return &MothServer{
Puzzles: puzzles,
2020-08-14 20:26:04 -06:00
Theme: theme,
State: state,
2020-03-01 16:10:55 -07:00
}
}
2020-08-17 17:43:57 -06:00
// NewHandler returns a new http.RequestHandler for the provided teamID.
2020-08-19 18:01:21 -06:00
func (s *MothServer) NewHandler(participantID, teamID string) MothRequestHandler {
2020-03-01 16:10:55 -07:00
return MothRequestHandler{
2020-08-19 18:01:21 -06:00
MothServer: s,
participantID: participantID,
teamID: teamID,
2020-03-01 16:10:55 -07:00
}
}
2020-08-17 17:43:57 -06:00
// MothRequestHandler provides http.RequestHandler for a MothServer.
2020-03-01 16:10:55 -07:00
type MothRequestHandler struct {
*MothServer
2020-08-19 18:01:21 -06:00
participantID string
teamID string
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
// PuzzlesOpen opens a file associated with a puzzle.
2020-03-01 16:10:55 -07:00
func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (ReadSeekCloser, time.Time, error) {
2020-08-17 17:43:57 -06:00
export := mh.ExportState()
for _, p := range export.Puzzles[cat] {
if p == points {
return mh.Puzzles.Open(cat, points, path)
}
}
2020-08-14 20:26:04 -06:00
return nil, time.Time{}, fmt.Errorf("Puzzle locked")
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
// ThemeOpen opens a file from a theme.
2020-03-01 16:10:55 -07:00
func (mh *MothRequestHandler) ThemeOpen(path string) (ReadSeekCloser, time.Time, error) {
return mh.Theme.Open(path)
}
2020-08-17 17:43:57 -06:00
// Register associates a team name with a team ID.
2020-03-01 16:10:55 -07:00
func (mh *MothRequestHandler) Register(teamName string) error {
2020-08-18 17:04:23 -06:00
// BUG(neale): Register returns an error if a team is already registered; it may make more sense to return success
2020-03-01 16:10:55 -07:00
if teamName == "" {
return fmt.Errorf("Empty team name")
}
2020-08-17 17:43:57 -06:00
return mh.State.SetTeamName(mh.teamID, teamName)
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
// CheckAnswer returns an error if answer is not a correct answer for puzzle points in category cat
2020-03-01 16:10:55 -07:00
func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string) error {
if err := mh.Puzzles.CheckAnswer(cat, points, answer); err != nil {
2020-08-19 18:01:21 -06:00
msg := fmt.Sprintf("BAD %s %s %s %d %s", mh.participantID, mh.teamID, cat, points, err.Error())
mh.State.LogEvent(msg)
return err
}
2020-08-14 20:26:04 -06:00
2020-08-17 17:43:57 -06:00
if err := mh.State.AwardPoints(mh.teamID, cat, points); err != nil {
2020-08-19 18:01:21 -06:00
msg := fmt.Sprintf("GOOD %s %s %s %d", mh.participantID, mh.teamID, cat, points)
mh.State.LogEvent(msg)
return err
}
2020-08-14 20:26:04 -06:00
return nil
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
// ExportState anonymizes team IDs and returns StateExport.
// If a teamID has been specified for this MothRequestHandler,
// the anonymized team name for this teamID has the special value "self".
// If not, the puzzles list is empty.
func (mh *MothRequestHandler) ExportState() *StateExport {
2020-03-01 16:10:55 -07:00
export := StateExport{}
2020-08-17 17:43:57 -06:00
teamName, _ := mh.State.TeamName(mh.teamID)
2020-08-14 20:26:04 -06:00
2020-03-01 16:10:55 -07:00
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()
2020-08-17 17:43:57 -06:00
exportIDs := map[string]string{mh.teamID: "self"}
2020-03-01 16:10:55 -07:00
maxSolved := map[string]int{}
2020-08-17 17:43:57 -06:00
export.PointsLog = make(award.List, len(pointsLog))
for logno, awd := range pointsLog {
if id, ok := exportIDs[awd.TeamID]; ok {
awd.TeamID = id
2020-03-01 16:10:55 -07:00
} else {
2020-08-17 17:43:57 -06:00
exportID := strconv.Itoa(logno)
name, _ := mh.State.TeamName(awd.TeamID)
awd.TeamID = exportID
exportIDs[awd.TeamID] = awd.TeamID
export.TeamNames[exportID] = name
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
export.PointsLog[logno] = awd
2020-08-14 20:26:04 -06:00
2020-03-01 16:10:55 -07:00
// Record the highest-value unlocked puzzle in each category
2020-08-17 17:43:57 -06:00
if awd.Points > maxSolved[awd.Category] {
maxSolved[awd.Category] = awd.Points
2020-03-01 16:10:55 -07:00
}
}
export.Puzzles = make(map[string][]int)
2020-08-17 17:43:57 -06:00
if _, ok := export.TeamNames["self"]; ok {
// We used to hand this out to everyone,
// but then we got a bad reputation on some secretive blacklist,
// and now the Navy can't register for events.
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
}
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
export.Puzzles[category.Name] = puzzles
2020-03-01 16:10:55 -07:00
}
}
return &export
}