diff --git a/remote/challenge-sync-airbus/api.go b/remote/challenge-sync-airbus/api.go index dd0dbd2e..9f4cc4a2 100644 --- a/remote/challenge-sync-airbus/api.go +++ b/remote/challenge-sync-airbus/api.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "strconv" ) @@ -32,6 +33,7 @@ func (a *AirbusAPI) request(method, endpoint string, data []byte, out interface{ return fmt.Errorf("unable to prepare request to %q: %w", endpoint, err) } + req.Header.Add("Authorization", "Bearer "+a.Token) req.Header.Add("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) @@ -40,7 +42,7 @@ func (a *AirbusAPI) request(method, endpoint string, data []byte, out interface{ } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusOK { if out != nil { jdec := json.NewDecoder(resp.Body) @@ -149,30 +151,44 @@ func (a *AirbusAPI) GetChallengeFromName(name string) (*AirbusChallenge, error) return nil, fmt.Errorf("unable to find challenge %q", name) } -func (a *AirbusAPI) ValidateChallengeFromUser(userId AirbusUserId, challengeId AirbusChallengeId) (err error) { - err = a.request("GET", fmt.Sprintf("/v1/sessions/%d/%s/%s/validate", a.SessionID, challengeId.String(), userId.String()), nil, nil) +func (a *AirbusAPI) ValidateChallengeFromUser(team *AirbusTeam, challengeId AirbusChallengeId) (err error) { + if dryRun { + log.Printf("ValidateChallenge: %d, %s, %d", a.SessionID, challengeId.String(), team.Members[0].ID) + return + } + + err = a.request("GET", fmt.Sprintf("/v1/sessions/%d/%s/%d/validate", a.SessionID, challengeId.String(), team.Members[0].ID), nil, nil) return } type AirbusUserAwards struct { - UserId AirbusUserId `json:"gaming_user_id"` - Message string `json:"name"` - Value int64 `json:"value"` + UserId int64 `json:"gaming_user_id"` + Message string `json:"name"` + Value int64 `json:"value"` } -func (a *AirbusAPI) AwardUser(userId AirbusUserId, value int64, message string) (err error) { +func (a *AirbusAPI) AwardUser(team *AirbusTeam, value int64, message string) (err error) { awards := AirbusUserAwards{ - UserId: userId, + UserId: team.Members[0].ID, Message: message, Value: value, } + if dryRun { + log.Printf("AwardUser: %v", awards) + return + } + j, err := json.Marshal(awards) if err != nil { return fmt.Errorf("unable to marshall JSON from awards struct: %w", err) } err = a.request("POST", fmt.Sprintf("/v1/sessions/%d/awards", a.SessionID), j, nil) + if err != nil { + return err + } + return } diff --git a/remote/challenge-sync-airbus/main.go b/remote/challenge-sync-airbus/main.go index fc5dc53d..de0f1941 100644 --- a/remote/challenge-sync-airbus/main.go +++ b/remote/challenge-sync-airbus/main.go @@ -18,11 +18,13 @@ import ( var ( TeamsDir string skipInitialSync bool + dryRun bool ) func main() { flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files") var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events") + flag.BoolVar(&dryRun, "dry-run", dryRun, "Don't perform any write action, just display") flag.BoolVar(&skipInitialSync, "skipinitialsync", skipInitialSync, "Skip the initial synchronization") flag.BoolVar(&noValidateChallenge, "no-validate-challenge", noValidateChallenge, "Consider challenge validation as a standard award (if each exercice hasn't been imported on their side)") daemon := flag.Bool("watch", false, "Enable daemon mode by watching the directory") @@ -41,13 +43,27 @@ func main() { if v, exists := os.LookupEnv("AIRBUS_TOKEN"); exists { api.Token = v } + if v, exists := os.LookupEnv("AIRBUS_SESSIONID"); exists { var err error api.SessionID, err = strconv.ParseInt(v, 10, 64) if err != nil { log.Fatal("AIRBUS_SESSIONID is invalid: ", err.Error()) } + } else if v, exists := os.LookupEnv("AIRBUS_SESSION_NAME"); exists { + sessions, err := api.GetSessions() + if err != nil { + log.Fatal("Unable to retrieve session: ", err) + } + + for _, session := range sessions { + if session.Name == v { + api.SessionID = session.ID + break + } + } } + if v, exists := os.LookupEnv("AIRBUS_SESSIONUUID"); exists { api.SessionUUID = v } @@ -62,12 +78,6 @@ func main() { log.Fatal("Unable to open timestamp file: ", err.Error()) } - // Load exercices bindings - exbindings, err := ReadExercicesBindings(*exercicespath) - if err != nil { - log.Fatal("Unable to open exercices bindings file: ", err.Error()) - } - // Load teams.json teamsbindings, err := getTeams(filepath.Join(TeamsDir, "teams.json")) if err != nil { @@ -75,18 +85,28 @@ func main() { } w := Walker{ - LastSync: ts, - Exercices: exbindings, - Teams: teamsbindings, - API: api, - Coeff: *coeff, + LastSync: ts, + Teams: teamsbindings, + API: api, + Coeff: *coeff, + } + if err = w.fetchTeams(); err != nil { + log.Fatal("Unable to fetch Airbus teams: ", err.Error()) + } + + if !noValidateChallenge { + // Load exercices bindings + w.Exercices, err = ReadExercicesBindings(*exercicespath) + if err != nil { + log.Fatal("Unable to open exercices bindings file: ", err.Error()) + } } if !skipInitialSync { // Iterate over teams scores err = filepath.WalkDir(TeamsDir, w.WalkScoreSync) if err != nil { - log.Printf("Something goes wrong during walking") + log.Println("Something goes wrong during walking: ", err.Error()) } // save current timestamp for teams @@ -131,6 +151,9 @@ func main() { return } w.Teams = teamsbindings + if err = w.fetchTeams(); err != nil { + log.Fatal("Unable to fetch teams: ", err.Error()) + } // save current timestamp for teams err = saveTS(*tspath, w.LastSync) @@ -160,6 +183,9 @@ func main() { return } w.Teams = teamsbindings + if err = w.fetchTeams(); err != nil { + log.Fatal("Unable to fetch teams: ", err.Error()) + } // FIXME @@ -183,6 +209,9 @@ func main() { return } w.Teams = teamsbindings + if err = w.fetchTeams(); err != nil { + log.Fatal("Unable to fetch teams: ", err.Error()) + } } } 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.") diff --git a/remote/challenge-sync-airbus/session.go b/remote/challenge-sync-airbus/session.go new file mode 100644 index 00000000..67c7b563 --- /dev/null +++ b/remote/challenge-sync-airbus/session.go @@ -0,0 +1,15 @@ +package main + +import () + +type Session struct { + Name string `json:"name"` + Status string `json:"status"` + ID int64 `json:"id"` + Mode string `json:"mode"` +} + +func (a *AirbusAPI) GetSessions() (ret []Session, err error) { + err = a.request("GET", "/api/v1/sessions", nil, &ret) + return +} diff --git a/remote/challenge-sync-airbus/team.go b/remote/challenge-sync-airbus/team.go new file mode 100644 index 00000000..69d5db23 --- /dev/null +++ b/remote/challenge-sync-airbus/team.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" +) + +type AirbusTeam struct { + ID int64 `json:"id"` + Members []TeamMember `json:"members"` + Name string `json:"name"` +} + +type TeamMember struct { + ID int64 `json:"id"` + Name string `json:"name"` + Nickname string `json:"nickname"` + EMail string `json:"email"` +} + +type airbusDataTeam struct { + Data []AirbusTeam `json:"data"` +} + +func (a *AirbusAPI) GetTeams() ([]AirbusTeam, error) { + var data airbusDataTeam + err := a.request("GET", fmt.Sprintf("/api/v1/sessions/%d/teams", a.SessionID), nil, &data) + if err != nil { + return nil, err + } else { + return data.Data, nil + } +} diff --git a/remote/challenge-sync-airbus/timestamp.go b/remote/challenge-sync-airbus/timestamp.go index 413f76e2..d9610366 100644 --- a/remote/challenge-sync-airbus/timestamp.go +++ b/remote/challenge-sync-airbus/timestamp.go @@ -6,10 +6,15 @@ import ( "time" ) -func loadTS(tspath string) (timestamp map[AirbusUserId]time.Time, err error) { +type TSValue struct { + Time time.Time `json:"t"` + Score int64 `json:"s"` +} + +func loadTS(tspath string) (timestamp map[string]*TSValue, err error) { var fd *os.File if _, err = os.Stat(tspath); os.IsNotExist(err) { - timestamp = map[AirbusUserId]time.Time{} + timestamp = map[string]*TSValue{} err = saveTS(tspath, timestamp) return } else if fd, err = os.Open(tspath); err != nil { @@ -26,7 +31,7 @@ func loadTS(tspath string) (timestamp map[AirbusUserId]time.Time, err error) { } } -func saveTS(tspath string, ts map[AirbusUserId]time.Time) error { +func saveTS(tspath string, ts map[string]*TSValue) error { if fd, err := os.Create(tspath); err != nil { return err } else { diff --git a/remote/challenge-sync-airbus/treat.go b/remote/challenge-sync-airbus/treat.go index d4b505a7..aefc8d8d 100644 --- a/remote/challenge-sync-airbus/treat.go +++ b/remote/challenge-sync-airbus/treat.go @@ -1,11 +1,11 @@ package main import ( + "encoding/json" "fmt" "log" "os" "path/filepath" - "time" "srs.epita.fr/fic-server/libfic" ) @@ -15,55 +15,144 @@ var ( ) type Walker struct { - LastSync map[AirbusUserId]time.Time - Exercices AirbusExercicesBindings - Teams map[string]fic.ExportedTeam - API AirbusAPI - Coeff float64 + LastSync map[string]*TSValue + Exercices AirbusExercicesBindings + Teams map[string]fic.ExportedTeam + RevTeams map[string]string + TeamBindings map[string]*AirbusTeam + API AirbusAPI + Coeff float64 +} + +func (w *Walker) fetchTeams() error { + teams, err := w.API.GetTeams() + if err != nil { + return err + } + + w.RevTeams = map[string]string{} + w.TeamBindings = map[string]*AirbusTeam{} + + for tid, team := range w.Teams { + for _, t := range teams { + if team.Name == t.Name || team.ExternalId == t.Name { + w.TeamBindings[tid] = &t + break + } + } + + if _, ok := w.TeamBindings[tid]; !ok { + log.Printf("Team binding not found: %s - %s", tid, team.Name) + } + + w.RevTeams[team.Name] = tid + } + + return nil } func (w *Walker) treat(path string) { - 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 { - log.Println("Unable to open my.json:", err) - return - } - defer fdmy.Close() + teamid := filepath.Base(filepath.Dir(path)) - teammy, err := fic.ReadMyJSON(fdmy) - if err != nil { - log.Println("Unable to parse my.json:", err) - return - } - - airbusTeamId := NewAirbusUserId(w.Teams[fmt.Sprintf("%d", teammy.Id)].ExternalId) - - // Treat score grid - /*err = w.TreatScoreGrid(path, airbusTeamId) - if err != nil { - log.Println("Unable to treat score grid:", err) - return - }*/ - - // Balance scores - err = w.BalanceScore(int64(float64(teammy.Points)*w.Coeff), airbusTeamId) - if err != nil { - log.Println("Unable to balance score:", err) - return - } + if _, ok := w.TeamBindings[teamid]; ok { + w.TreatScoreGrid(path, w.TeamBindings[teamid]) } } +func (w *Walker) LoadScoreState(path string) (int64, error) { + mypath := filepath.Join(filepath.Dir(path), "airbus.json") + if _, err := os.Stat(mypath); os.IsNotExist(err) { + fd, err := os.Create(mypath) + if err != nil { + return 0, err + } + defer fd.Close() + + fd.Write([]byte("0")) + return 0, nil + } + + fd, err := os.Open(mypath) + if err != nil { + return 0, err + } + defer fd.Close() + + var ret int64 + + jdec := json.NewDecoder(fd) + if err := jdec.Decode(&ret); err != nil { + return 0, fmt.Errorf("an error occurs when trying to decode airbus.json: %w", err) + } + + return ret, nil +} + +func (w *Walker) LoadScoreGrid(path string) ([]fic.ScoreGridRow, error) { + fd, err := os.Open(filepath.Join(filepath.Dir(path), "scores.json")) + if err != nil { + return nil, err + } + defer fd.Close() + + var ret []fic.ScoreGridRow + + jdec := json.NewDecoder(fd) + if err := jdec.Decode(&ret); err != nil { + return nil, fmt.Errorf("an error occurs when trying to decode airbus.json: %w", err) + } + + return ret, nil +} + func (w *Walker) WalkScoreSync(path string, d os.DirEntry, err error) error { if filepath.Base(path) == "scores.json" { w.treat(path) } + + for team, ts := range w.LastSync { + team_id, ok := w.RevTeams[team] + if !ok { + continue + } + + myteam, err := w.loadMyFile(filepath.Join(TeamsDir, team_id, "my.json")) + if err != nil { + return fmt.Errorf("Unable to open %s/my.json: %w", team_id, err) + } + + airbusTeam := w.TeamBindings[fmt.Sprintf("%d", myteam.Id)] + + if ts.Score != myteam.Points*int64(w.Coeff) { + err := w.API.AwardUser(airbusTeam, myteam.Points-ts.Score, "Équilibrage") + if err != nil { + return fmt.Errorf("Unable to open %s/my.json: %w", team, err) + } + + w.LastSync[airbusTeam.Name].Score = myteam.Points * int64(w.Coeff) + } + } + return nil } +func (w *Walker) loadMyFile(path string) (*fic.MyTeam, error) { + fd, err := os.Open(path) + if err != nil { + return nil, err + } + defer fd.Close() + + var ret fic.MyTeam + + jdec := json.NewDecoder(fd) + if err := jdec.Decode(&ret); err != nil { + return nil, fmt.Errorf("an error occurs when trying to decode airbus.json: %w", err) + } + + return &ret, nil +} + func (w *Walker) WalkScore(path string, d os.DirEntry, err error) error { if filepath.Base(path) == "scores.json" { go w.treat(path) @@ -71,7 +160,7 @@ func (w *Walker) WalkScore(path string, d os.DirEntry, err error) error { return nil } -func (w *Walker) TreatScoreGrid(path string, airbusTeamId AirbusUserId) error { +func (w *Walker) TreatScoreGrid(path string, airbusTeam *AirbusTeam) error { // Read score grid fdscores, err := os.Open(path) if err != nil { @@ -85,30 +174,35 @@ func (w *Walker) TreatScoreGrid(path string, airbusTeamId AirbusUserId) error { } // Found all new entries - maxts := w.LastSync[airbusTeamId] + maxts := &TSValue{} + if ts, ok := w.LastSync[airbusTeam.Name]; ok { + maxts = ts + } for _, row := range teamscores { - if row.Time.After(maxts) { - maxts = row.Time + if row.Time.After(maxts.Time) { + maxts.Time = row.Time } - if row.Time.After(w.LastSync[airbusTeamId]) { + if row.Time.After(w.LastSync[airbusTeam.Name].Time) { if !noValidateChallenge && row.Reason == "Validation" { - err = w.API.ValidateChallengeFromUser(airbusTeamId, w.Exercices[row.IdExercice]) + err = w.API.ValidateChallengeFromUser(airbusTeam, w.Exercices[row.IdExercice]) } else { - err = w.API.AwardUser(airbusTeamId, int64(row.Points*row.Coeff*w.Coeff), row.Reason) + err = w.API.AwardUser(airbusTeam, int64(row.Points*row.Coeff*w.Coeff), row.Reason) } if err != nil { return err } + + maxts.Score += int64(row.Points * row.Coeff * w.Coeff) } } - w.LastSync[airbusTeamId] = maxts + w.LastSync[airbusTeam.Name] = maxts return nil } -func (w *Walker) BalanceScore(score int64, airbusTeamId AirbusUserId) error { +func (w *Walker) BalanceScore(score int64, airbusTeam *AirbusTeam) error { // Read current score on other platform stats, err := w.API.GetCurrentStats() if err != nil { @@ -120,9 +214,9 @@ func (w *Walker) BalanceScore(score int64, airbusTeamId AirbusUserId) error { return fmt.Errorf("session not found") } - other_team := my_session.GetTeam(AirbusUUID(airbusTeamId)) + other_team := my_session.GetTeam(AirbusUUID(airbusTeam.Name)) if other_team == nil { - return fmt.Errorf("team %q not found", airbusTeamId) + return fmt.Errorf("team %q not found", airbusTeam.Name) } other_score := other_team.Score @@ -130,7 +224,7 @@ func (w *Walker) BalanceScore(score int64, airbusTeamId AirbusUserId) error { // Send diff to the platform if other_score != score { diff := score - other_score - return w.API.AwardUser(airbusTeamId, diff, "Équilibrage") + return w.API.AwardUser(airbusTeam, diff, "Équilibrage") } return nil