diff --git a/remote/challenge-sync-airbus/api.go b/remote/challenge-sync-airbus/api.go index 9b01cfe5..d2bdd0cf 100644 --- a/remote/challenge-sync-airbus/api.go +++ b/remote/challenge-sync-airbus/api.go @@ -12,7 +12,7 @@ import ( type AirbusAPI struct { BaseURL string Token string - SessionID uint64 + SessionID int64 } func (a *AirbusAPI) request(method, endpoint string, data []byte, out interface{}) error { @@ -52,6 +52,11 @@ func (a *AirbusAPI) request(method, endpoint string, data []byte, out interface{ type AirbusUserId int64 +func NewAirbusUserId(externalid string) AirbusUserId { + v, _ := strconv.ParseInt(externalid, 10, 64) + return AirbusUserId(v) +} + func (aui AirbusUserId) String() string { return strconv.FormatInt(int64(aui), 10) } @@ -112,8 +117,8 @@ func (a *AirbusAPI) GetChallengeFromName(name string) (*AirbusChallenge, error) return nil, fmt.Errorf("unable to find challenge %q", name) } -func (a *AirbusAPI) ValidateChallengeFromUser(user *AirbusUser, challenge *AirbusChallenge) (err error) { - err = a.request("GET", fmt.Sprintf("/sessions/%d/%s/%s/validate", a.SessionID, challenge.Id.String(), user.Id.String()), nil, nil) +func (a *AirbusAPI) ValidateChallengeFromUser(userId AirbusUserId, challengeId AirbusChallengeId) (err error) { + err = a.request("GET", fmt.Sprintf("/sessions/%d/%s/%s/validate", a.SessionID, challengeId.String(), userId.String()), nil, nil) return } @@ -123,9 +128,9 @@ type AirbusUserAwards struct { Value int64 `json:"value"` } -func (a *AirbusAPI) AwardUser(user *AirbusUser, value int64, message string) (err error) { +func (a *AirbusAPI) AwardUser(userId AirbusUserId, value int64, message string) (err error) { awards := AirbusUserAwards{ - UserId: user.Id, + UserId: userId, Message: message, Value: value, } @@ -147,6 +152,18 @@ type AirbusStatsData struct { BySessions []AirbusStatsSession `json:"by_sessions"` } +func (s AirbusStatsData) GetSession(sessionId AirbusUUID) *AirbusStatsSession { + for _, session := range s.BySessions { + if session.UUID == sessionId { + return &session + } + } + + return nil +} + +type AirbusUUID string + type AirbusStatsSession struct { UUID AirbusUUID `json:"uuid"` Name string `json:"name"` @@ -154,6 +171,16 @@ type AirbusStatsSession struct { TeamStats []AirbusTeamStats `json:"team_stats"` } +func (s AirbusStatsSession) GetTeam(teamId AirbusUUID) *AirbusTeamStats { + for _, team := range s.TeamStats { + if team.UUID == teamId { + return &team + } + } + + return nil +} + type AirbusTeamStats struct { UUID AirbusUUID `json:"uuid"` Name string `json:"name"` diff --git a/remote/challenge-sync-airbus/bindings.go b/remote/challenge-sync-airbus/bindings.go new file mode 100644 index 00000000..f7af582b --- /dev/null +++ b/remote/challenge-sync-airbus/bindings.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "os" + + "srs.epita.fr/fic-server/libfic" +) + +type AirbusExercicesBindings map[int64]AirbusChallengeId + +func ReadExercicesBindings(ebpath string) (AirbusExercicesBindings, error) { + fd, err := os.Open(ebpath) + if err != nil { + return nil, err + } + defer fd.Close() + + jdec := json.NewDecoder(fd) + + var aeb AirbusExercicesBindings + err = jdec.Decode(&aeb) + + return aeb, err +} + +func getTeams(pathname string) (teams map[string]fic.ExportedTeam, err error) { + var cnt_raw []byte + if cnt_raw, err = ioutil.ReadFile(pathname); err != nil { + return + } + + if err = json.Unmarshal(cnt_raw, &teams); err != nil { + return + } + + return +} diff --git a/remote/challenge-sync-airbus/main.go b/remote/challenge-sync-airbus/main.go index 3c873c10..75078360 100644 --- a/remote/challenge-sync-airbus/main.go +++ b/remote/challenge-sync-airbus/main.go @@ -4,12 +4,10 @@ import ( "flag" "log" "os" - "os/signal" "path" + "path/filepath" "strconv" - "syscall" - - "gopkg.in/fsnotify.v1" + "time" ) var ( @@ -19,8 +17,12 @@ var ( func main() { flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files") - var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events") + //var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events") flag.BoolVar(&skipInitialSync, "skipinitialsync", skipInitialSync, "Skip the initial synchronization") + //watcher := flag.Bool("watch", false, "Enable daemon mode by watching the directory") + tspath := flag.String("timestamp-file", "./REMOTE/timestamp", "Path to the file storing the last timestamp") + exercicespath := flag.String("exercices-file", "./REMOTE/exercices-bindings.json", "Path to the file containing the ID bindings") + coeff := flag.Float64("global-coeff", 10.0, "Coefficient to use to multiply all scores before passing them to the other platform") flag.Parse() api := AirbusAPI{ @@ -35,7 +37,7 @@ func main() { } if v, exists := os.LookupEnv("AIRBUS_SESSIONID"); exists { var err error - api.SessionID, err = strconv.ParseUint(v, 10, 64) + api.SessionID, err = strconv.ParseInt(v, 10, 64) if err != nil { log.Fatal("AIRBUS_SESSIONID is invalid: ", err.Error()) } @@ -45,54 +47,43 @@ func main() { TeamsDir = path.Clean(TeamsDir) - log.Println("Registering directory events...") - watcher, err := fsnotify.NewWatcher() + // Load the timestamp + ts, err := loadTS(*tspath) if err != nil { - log.Fatal(err) - } - defer watcher.Close() - - if err := watcher.Add(TeamsDir); err != nil { - log.Fatal(err) + log.Fatal("Unable to open timestamp file: ", err.Error()) } - if !skipInitialSync { - if _, err := os.Stat(path.Join(TeamsDir, "teams.json")); err == nil { - treatAll(path.Join(TeamsDir, "teams.json")) - } + // Load exercices bindings + exbindings, err := ReadExercicesBindings(*exercicespath) + if err != nil { + log.Fatal("Unable to open exercices bindings file: ", err.Error()) } - // Register SIGUSR1, SIGUSR2 - interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, syscall.SIGHUP) + // Load teams.json + teamsbindings, err := getTeams(filepath.Join(TeamsDir, "teams.json")) + if err != nil { + log.Fatal("Unable to open teams bindings file: ", err.Error()) + } - watchedNotify := fsnotify.Create + w := Walker{ + LastSync: *ts, + Exercices: exbindings, + Teams: teamsbindings, + API: api, + Coeff: *coeff, + } - for { - select { - case <-interrupt: - log.Println("SIGHUP received, resyncing all teams' score...") - treatAll(path.Join(TeamsDir, "teams.json")) - log.Println("SIGHUP treated.") - case ev := <-watcher.Events: - if path.Base(ev.Name) == "teams.json" { - if ev.Op&watchedNotify == watchedNotify { - if *debugINotify { - log.Println("Treating event:", ev, "for", ev.Name) - } - go treatDiff(ev.Name) - } else if ev.Op&fsnotify.Write == fsnotify.Write { - log.Println("FSNOTIFY WRITE SEEN. Prefer looking at them, as it appears files are not atomically moved.") - watchedNotify = fsnotify.Write - go treatDiff(ev.Name) - } else if *debugINotify { - log.Println("Skipped teams.json event:", ev) - } - } else if *debugINotify { - log.Println("Skipped NON teams.json event:", ev, "for", ev.Name) - } - case err := <-watcher.Errors: - log.Println("error:", err) - } + // Iterate over teams scores + err = filepath.WalkDir(TeamsDir, w.WalkScore) + if err != nil { + log.Printf("Something goes wrong during walking") + } + + // Update timestamp for the next time + w.LastSync = time.Now() + + err = saveTS(*tspath, &w.LastSync) + if err != nil { + log.Fatal("Unable to save timestamp file: ", err.Error()) } } diff --git a/remote/challenge-sync-airbus/timestamp.go b/remote/challenge-sync-airbus/timestamp.go new file mode 100644 index 00000000..1b31d963 --- /dev/null +++ b/remote/challenge-sync-airbus/timestamp.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "os" + "time" +) + +func loadTS(tspath string) (timestamp *time.Time, err error) { + if _, err = os.Stat(tspath); os.IsNotExist(err) { + init := time.Unix(0, 0) + timestamp = &init + + err = saveTS(tspath, timestamp) + return + } + + var fd *os.File + fd, err = os.Open(tspath) + if err != nil { + return + } + defer fd.Close() + + var init int64 + _, err = fmt.Fscanf(fd, "%d", &init) + if err != nil { + return + } + + tmp := time.Unix(init, 0) + timestamp = &tmp + + return +} + +func saveTS(tspath string, ts *time.Time) error { + fd, err := os.Create(tspath) + if err != nil { + return err + } + defer fd.Close() + + _, err = fmt.Fprintf(fd, "%d", ts.Unix()) + if err != nil { + return err + } + + return nil +} diff --git a/remote/challenge-sync-airbus/treat.go b/remote/challenge-sync-airbus/treat.go new file mode 100644 index 00000000..769c5ba1 --- /dev/null +++ b/remote/challenge-sync-airbus/treat.go @@ -0,0 +1,112 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "srs.epita.fr/fic-server/libfic" +) + +type Walker struct { + LastSync time.Time + Exercices AirbusExercicesBindings + Teams map[string]fic.ExportedTeam + API AirbusAPI + Coeff float64 +} + +func (w *Walker) WalkScore(path string, d os.DirEntry, err error) error { + if filepath.Base(path) == "scores.json" { + mypath := filepath.Join(filepath.Dir(path), "my.json") + if _, err := os.Stat(mypath); !os.IsNotExist(err) { + // Read team ID + fdmy, err := os.Open(mypath) + if err != nil { + return err + } + defer fdmy.Close() + + teammy, err := fic.ReadMyJSON(fdmy) + if err != nil { + return err + } + + airbusTeamId := NewAirbusUserId(w.Teams[fmt.Sprintf("%d", teammy.Id)].ExternalId) + + // Treat score grid + err = w.TreatScoreGrid(path, airbusTeamId) + if err != nil { + return err + } + + // Balance scores + err = w.BalanceScore(int64(float64(teammy.Points)*w.Coeff), airbusTeamId) + if err != nil { + return err + } + } + } + + return nil +} + +func (w *Walker) TreatScoreGrid(path string, airbusTeamId AirbusUserId) error { + // Read score grid + fdscores, err := os.Open(path) + if err != nil { + return err + } + defer fdscores.Close() + + teamscores, err := fic.ReadScoreGrid(fdscores) + if err != nil { + return err + } + + // Found all new entries + for _, row := range teamscores { + if row.Time.After(w.LastSync) { + if row.Reason == "Validation" { + err = w.API.ValidateChallengeFromUser(airbusTeamId, w.Exercices[row.IdExercice]) + } else { + err = w.API.AwardUser(airbusTeamId, int64(row.Points*row.Coeff*w.Coeff), row.Reason) + } + + if err != nil { + return err + } + } + } + + return nil +} + +func (w *Walker) BalanceScore(score int64, airbusTeamId AirbusUserId) error { + // Read current score on other platform + stats, err := w.API.GetCurrentStats() + if err != nil { + fmt.Errorf("unable to retrieve current stats: %w", err) + } + + my_session := stats.Data.GetSession(AirbusUUID(w.API.SessionID)) + if my_session == nil { + return fmt.Errorf("session not found") + } + + other_team := my_session.GetTeam(AirbusUUID(airbusTeamId)) + if other_team == nil { + return fmt.Errorf("team %q not found", airbusTeamId) + } + + other_score := other_team.Score + + // Send diff to the platform + if other_score != score { + diff := score - other_score + return w.API.AwardUser(airbusTeamId, diff, "Équilibrage") + } + + return nil +}