diff --git a/cmd/mothd/httpd.go b/cmd/mothd/httpd.go index 54ca156..1963f63 100644 --- a/cmd/mothd/httpd.go +++ b/cmd/mothd/httpd.go @@ -125,6 +125,22 @@ func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWrite } } +// AssignParticipantHandler handles attempts to associate a participant with a team +func (h *HTTPServer) AssignParticipantHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { + if mh.participantID == "" { + jsend.Sendf(w, jsend.Fail, "empty name", "Participant ID may not be empty") + return + } + + if err := mh.AssignParticipant(); err != ErrAlreadyRegistered { + jsend.Sendf(w, jsend.Success, "already assigned", "participant and team have already been associated") + } else if err != nil { + jsend.Sendf(w, jsend.Fail, "unable to associate participant and team", err.Error()) + } else { + jsend.Sendf(w, jsend.Success, "assigned", "participant and team have been associated") + } +} + // AnswerHandler checks answer correctness and awards points func (h *HTTPServer) AnswerHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) { cat := req.FormValue("cat") diff --git a/cmd/mothd/server.go b/cmd/mothd/server.go index 608e6d6..11f66c0 100644 --- a/cmd/mothd/server.go +++ b/cmd/mothd/server.go @@ -57,6 +57,7 @@ type StateProvider interface { PointsLog() award.List TeamName(teamID string) (string, error) SetTeamName(teamID, teamName string) error + AssignParticipant(participantID string, teamID string) error AwardPoints(teamID string, cat string, points int) error LogEvent(event, participantID, teamID, cat string, points int, extra ...string) Maintainer @@ -176,6 +177,20 @@ func (mh *MothRequestHandler) Register(teamName string) error { return mh.State.SetTeamName(mh.teamID, teamName) } +// AssignParticipant associates a participant with a team +func (mh *MothRequestHandler) AssignParticipant() error { + if mh.participantID == "" { + return fmt.Errorf("empty participant ID") + } + + if mh.teamID == "" { + return fmt.Errorf("empty participant ID") + } + + mh.State.LogEvent("assign", mh.participantID, mh.teamID, "", 0) + return mh.State.AssignParticipant(mh.participantID, mh.teamID) +} + // 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". diff --git a/cmd/mothd/state.go b/cmd/mothd/state.go index 9d11621..037d82a 100644 --- a/cmd/mothd/state.go +++ b/cmd/mothd/state.go @@ -46,6 +46,7 @@ type State struct { // Caches, so we're not hammering NFS with metadata operations teamNames map[string]string + participantTeams map[string]string pointsLog award.List messages string lock sync.RWMutex @@ -60,6 +61,7 @@ func NewState(fs afero.Fs) *State { eventStream: make(chan []string, 80), teamNames: make(map[string]string), + participantTeams: make(map[string]string), } if err := s.reopenEventLog(); err != nil { log.Fatal(err) @@ -175,6 +177,48 @@ func (s *State) SetTeamName(teamID, teamName string) error { return nil } +// AssignParticipant associated a participant with a team +// A participant can only be associated with one team at a time +func (s *State) AssignParticipant(participantID string, teamID string) error { + idsFile, err := s.Open("participantids.txt") + if err != nil { + return fmt.Errorf("participant IDs file does not exist") + } + defer idsFile.Close() + found := false + scanner := bufio.NewScanner(idsFile) + for scanner.Scan() { + if scanner.Text() == participantID { + found = true + break + } + } + if !found { + return fmt.Errorf("participant ID not found in list of valid participant IDs") + } + + _, err = s.TeamName(teamID) + + if (err != nil) { + return fmt.Errorf("Provided team does not exist, or is not registered") + } + + teamParticipantFilename := filepath.Join("participants", participantID) + teamParticipantFile, err := s.Fs.OpenFile(teamParticipantFilename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644) + if os.IsExist(err) { + return ErrAlreadyRegistered + } else if err != nil { + return err + } + defer teamParticipantFile.Close() + log.Printf("Adding participant [%s] to team [%s]", participantID, teamID) + fmt.Fprintln(teamParticipantFile, teamID) + + s.refreshNow <- true + + return nil +} + // PointsLog retrieves the current points log. func (s *State) PointsLog() award.List { s.lock.RLock() @@ -308,6 +352,7 @@ func (s *State) maybeInitialize() { s.RemoveAll("points.tmp") s.RemoveAll("points.new") s.RemoveAll("teams") + s.RemoveAll("participants") // Open log file if err := s.reopenEventLog(); err != nil { @@ -319,6 +364,7 @@ func (s *State) maybeInitialize() { s.Mkdir("points.tmp", 0755) s.Mkdir("points.new", 0755) s.Mkdir("teams", 0755) + s.Mkdir("participants", 0755) // Preseed available team ids if file doesn't exist if f, err := s.OpenFile("teamids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { @@ -333,6 +379,19 @@ func (s *State) maybeInitialize() { f.Close() } + // Preseed available participants if file doesn't exist + if f, err := s.OpenFile("participantids.txt", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644); err == nil { + id := make([]byte, 16) + for i := 0; i < 100; i++ { + for i := range id { + char := rand.Intn(len(DistinguishableChars)) + id[i] = DistinguishableChars[char] + } + fmt.Fprintln(f, string(id)) + } + f.Close() + } + // Create some files if f, err := s.Create("initialized"); err == nil { fmt.Fprintln(f, "initialized: remove to re-initialize the contest.") @@ -451,6 +510,28 @@ func (s *State) updateCaches() { } + { + // Update participant records + for k := range s.participantTeams { + delete(s.participantTeams, k) + } + + participantsFs := afero.NewBasePathFs(s.Fs, "participants") + if dirents, err := afero.ReadDir(participantsFs, "."); err != nil { + log.Printf("Reading participant ids: %v", err) + } else { + for _, dirent := range dirents { + participantID := dirent.Name() + if participantTeamBytes, err := afero.ReadFile(participantsFs, participantID); err != nil { + log.Printf("Reading participant %s: %v", participantID, err) + } else { + teamID := strings.TrimSpace(string(participantTeamBytes)) + s.participantTeams[participantID] = teamID + } + } + } + } + if bMessages, err := afero.ReadFile(s, "messages.html"); err == nil { s.messages = string(bMessages) }