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-09-08 17:49:02 -06:00
// Configuration stores information about server configuration.
type Configuration struct {
Devel bool
}
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 {
2020-09-08 17:49:02 -06:00
Config Configuration
2020-03-01 16:10:55 -07:00
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
2020-09-08 17:49:02 -06:00
CheckAnswer ( cat string , points int , answer string ) ( bool , error )
2020-10-12 17:44:44 -06:00
Mothball ( cat string , w io . Writer ) 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 {
2021-12-30 13:17:17 -07:00
Messages ( ) string // GET /admin/state/messages
SetMessages ( string ) error // POST /admin/state/messages
PointsLog ( ) award . List // GET /admin/state/points
AwardPoints ( teamID string , cat string , points int ) error // POST /admin/state/points
2021-12-30 18:00:14 -07:00
AwardPointsAtTime ( teamID string , cat string , points int , when int64 ) error // POST /admin/state/points
PointExists ( teamID string , cat string , points int ) bool // Check if point entry exists // HEAD /admin/state/points/<teamID>/<cat>/<points>
PointExistsAtTime ( teamID string , cat string , points int , when int64 ) bool // Check if point entry exists // HEAD /admin/state/points/<teamID>/<cat>/<points>/<when>
RemovePoints ( teamID string , cat string , points int ) error // DELETE /admin/state/points/<teamID>/<cat>/<points>
RemovePointsAtTime ( teamID string , cat string , points int , when int64 ) error // DELETE /admin/state/points/<teamID>/<cat>/<points>/<when>
SetPoints ( award . List ) error // PUT /admin/state/points
2021-12-30 13:17:17 -07:00
2021-12-30 18:00:14 -07:00
TeamIDs ( ) ( [ ] string , error ) // GET /admin/state/team_ids
SetTeamIDs ( [ ] string ) error // PUT /admin/state/team_ids
AddTeamID ( teamID string ) error // POST /admin/state/team_ids/<teamID>
RemoveTeamID ( teamID string ) error // DELETE /admin/state/team_ids/<teamID>
TeamIDExists ( teamID string ) ( bool , error ) // HEAD /admin/state/team_ids/<teamID>
2021-12-30 13:17:17 -07:00
2021-12-30 18:00:14 -07:00
TeamNames ( ) map [ string ] string // GET /admin/state/teams
SetTeamNames ( map [ string ] string ) error // PUT /admin/state/teams
2021-12-30 13:17:17 -07:00
TeamName ( teamID string ) ( string , error ) // GET /admin/state/teams/id/<teamID>
2021-12-30 18:00:14 -07:00
TeamIDFromName ( teamName string ) ( string , error ) // GET /admin/state/teams/name/<teamName>
2021-12-30 13:17:17 -07:00
SetTeamName ( teamID , teamName string ) error // POST /admin/state/teams/id/<teamID>/<teamName>
2021-12-30 18:00:14 -07:00
DeleteTeamName ( teamID string ) error // DELETE /admin/state/teams/id/<teamID>, /admin/state/teams/name/<teamName>
2021-12-30 13:17:17 -07:00
2020-10-14 18:20:49 -06:00
LogEvent ( event , participantID , teamID , cat string , points int , extra ... 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 )
2021-10-20 11:29:55 -06:00
// refresh is a shortcut used internally for testing
refresh ( )
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 {
2020-09-08 17:49:02 -06:00
PuzzleProviders [ ] PuzzleProvider
Theme ThemeProvider
State StateProvider
Config Configuration
2020-03-01 16:10:55 -07:00
}
2020-08-17 17:43:57 -06:00
// NewMothServer returns a new MothServer.
2020-09-08 17:49:02 -06:00
func NewMothServer ( config Configuration , theme ThemeProvider , state StateProvider , puzzleProviders ... PuzzleProvider ) * MothServer {
2020-03-01 16:10:55 -07:00
return & MothServer {
2020-09-08 17:49:02 -06:00
Config : config ,
PuzzleProviders : puzzleProviders ,
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-09-08 17:49:02 -06:00
// BUG(neale): Multiple providers with the same category name are not detected or handled well.
func ( mh * MothRequestHandler ) PuzzlesOpen ( cat string , points int , path string ) ( r ReadSeekCloser , ts time . Time , err error ) {
2020-10-13 18:33:12 -06:00
export := mh . exportStateIfRegistered ( true )
2020-09-08 17:49:02 -06:00
found := false
2020-03-01 17:01:01 -07:00
for _ , p := range export . Puzzles [ cat ] {
if p == points {
2020-09-08 17:49:02 -06:00
found = true
}
}
if ! found {
2021-10-13 18:25:27 -06:00
return nil , time . Time { } , fmt . Errorf ( "puzzle does not exist or is locked" )
2020-09-08 17:49:02 -06:00
}
// Try every provider until someone doesn't return an error
for _ , provider := range mh . PuzzleProviders {
r , ts , err = provider . Open ( cat , points , path )
if err != nil {
return r , ts , err
2020-03-01 17:01:01 -07:00
}
}
2020-08-14 20:26:04 -06:00
2020-10-14 18:20:49 -06:00
// Log puzzle.json loads
if path == "puzzle.json" {
2020-10-15 10:23:39 -06:00
mh . State . LogEvent ( "load" , mh . participantID , mh . teamID , cat , points )
2020-10-14 18:20:49 -06:00
}
2020-09-08 17:49:02 -06:00
return
}
// CheckAnswer returns an error if answer is not a correct answer for puzzle points in category cat
func ( mh * MothRequestHandler ) CheckAnswer ( cat string , points int , answer string ) error {
correct := false
for _ , provider := range mh . PuzzleProviders {
if ok , err := provider . CheckAnswer ( cat , points , answer ) ; err != nil {
return err
} else if ok {
correct = true
}
}
if ! correct {
2020-10-14 18:20:49 -06:00
mh . State . LogEvent ( "wrong" , mh . participantID , mh . teamID , cat , points )
2021-10-13 18:25:27 -06:00
return fmt . Errorf ( "incorrect answer" )
2020-09-08 17:49:02 -06:00
}
2020-10-15 10:23:39 -06:00
mh . State . LogEvent ( "correct" , mh . participantID , mh . teamID , cat , points )
2020-09-08 17:49:02 -06:00
2020-12-02 19:31:34 -07:00
if _ , err := mh . State . TeamName ( mh . teamID ) ; err != nil {
2021-10-13 18:25:27 -06:00
return fmt . Errorf ( "invalid team ID" )
2020-12-02 19:31:34 -07:00
}
2020-09-08 17:49:02 -06:00
if err := mh . State . AwardPoints ( mh . teamID , cat , points ) ; err != nil {
2021-10-13 18:25:27 -06:00
return fmt . Errorf ( "error awarding points: %s" , err )
2020-09-08 17:49:02 -06:00
}
return nil
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 == "" {
2021-10-13 18:25:27 -06:00
return fmt . Errorf ( "empty team name" )
2020-03-01 16:10:55 -07:00
}
2020-10-28 14:16:13 -06:00
mh . State . LogEvent ( "register" , mh . participantID , mh . teamID , "" , 0 )
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
// 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-10-13 18:33:12 -06:00
return mh . exportStateIfRegistered ( false )
}
2022-05-10 17:47:26 -06:00
// Export state, replacing the team ID with "self" if the team is registered.
//
// If forceRegistered is true, go ahead and export it anyway
func ( mh * MothRequestHandler ) exportStateIfRegistered ( forceRegistered bool ) * StateExport {
2020-03-01 16:10:55 -07:00
export := StateExport { }
2020-09-08 17:49:02 -06:00
export . Config = mh . Config
2020-03-01 16:10:55 -07:00
2020-10-13 16:41:50 -06:00
teamName , err := mh . State . TeamName ( mh . teamID )
2022-05-10 17:47:26 -06:00
registered := forceRegistered || mh . Config . Devel || ( err == nil )
2020-08-14 20:26:04 -06:00
2020-03-01 16:10:55 -07:00
export . Messages = mh . State . Messages ( )
2020-10-14 10:05:53 -06:00
export . TeamNames = make ( map [ string ] string )
2020-03-01 16:10:55 -07:00
// Anonymize team IDs in points log, and write out team names
pointsLog := mh . State . PointsLog ( )
2020-10-14 10:04:13 -06:00
exportIDs := make ( map [ string ] string )
maxSolved := make ( map [ string ] int )
2020-08-17 17:43:57 -06:00
export . PointsLog = make ( award . List , len ( pointsLog ) )
2020-10-14 10:04:13 -06:00
if registered {
export . TeamNames [ "self" ] = teamName
exportIDs [ mh . teamID ] = "self"
}
2020-08-17 17:43:57 -06:00
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 )
2020-10-14 09:52:19 -06:00
exportIDs [ awd . TeamID ] = exportID
2020-10-14 09:46:51 -06:00
awd . TeamID = exportID
2020-08-17 17:43:57 -06:00
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-10-13 16:41:50 -06:00
if registered {
2020-08-17 17:43:57 -06:00
// 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.
2020-09-08 17:49:02 -06:00
for _ , provider := range mh . PuzzleProviders {
for _ , category := range provider . Inventory ( ) {
// Append sentry (end of puzzles)
allPuzzles := append ( category . Puzzles , 0 )
2020-08-17 17:43:57 -06:00
2020-09-08 17:49:02 -06:00
max := maxSolved [ category . Name ]
2020-08-17 17:43:57 -06:00
2020-09-08 17:49:02 -06:00
puzzles := make ( [ ] int , 0 , len ( allPuzzles ) )
for i , val := range allPuzzles {
puzzles = allPuzzles [ : i + 1 ]
if ! mh . Config . Devel && ( val > max ) {
break
}
2020-08-17 17:43:57 -06:00
}
2020-09-08 17:49:02 -06:00
export . Puzzles [ category . Name ] = puzzles
2020-03-01 16:10:55 -07:00
}
}
}
return & export
2020-03-01 17:01:01 -07:00
}
2020-09-15 15:58:21 -06:00
// Mothball generates a mothball for the given category.
2020-10-12 17:44:44 -06:00
func ( mh * MothRequestHandler ) Mothball ( cat string , w io . Writer ) error {
var err error
2020-09-15 15:58:21 -06:00
if ! mh . Config . Devel {
2021-10-13 18:25:27 -06:00
return fmt . Errorf ( "cannot mothball in production mode" )
2020-09-15 15:58:21 -06:00
}
for _ , provider := range mh . PuzzleProviders {
2020-10-12 17:44:44 -06:00
if err = provider . Mothball ( cat , w ) ; err == nil {
return nil
2020-09-15 15:58:21 -06:00
}
}
2020-10-12 17:44:44 -06:00
return err
2020-09-15 15:58:21 -06:00
}