challenge-sync-airbus: Do job

This commit is contained in:
nemunaire 2023-04-06 03:48:52 +02:00
parent 18b8f0f722
commit 3344e05e0d
6 changed files with 262 additions and 71 deletions

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"strconv" "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) 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") req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req) 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() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode == http.StatusOK {
if out != nil { if out != nil {
jdec := json.NewDecoder(resp.Body) 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) return nil, fmt.Errorf("unable to find challenge %q", name)
} }
func (a *AirbusAPI) ValidateChallengeFromUser(userId AirbusUserId, challengeId AirbusChallengeId) (err error) { func (a *AirbusAPI) ValidateChallengeFromUser(team *AirbusTeam, challengeId AirbusChallengeId) (err error) {
err = a.request("GET", fmt.Sprintf("/v1/sessions/%d/%s/%s/validate", a.SessionID, challengeId.String(), userId.String()), nil, nil) 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 return
} }
type AirbusUserAwards struct { type AirbusUserAwards struct {
UserId AirbusUserId `json:"gaming_user_id"` UserId int64 `json:"gaming_user_id"`
Message string `json:"name"` Message string `json:"name"`
Value int64 `json:"value"` 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{ awards := AirbusUserAwards{
UserId: userId, UserId: team.Members[0].ID,
Message: message, Message: message,
Value: value, Value: value,
} }
if dryRun {
log.Printf("AwardUser: %v", awards)
return
}
j, err := json.Marshal(awards) j, err := json.Marshal(awards)
if err != nil { if err != nil {
return fmt.Errorf("unable to marshall JSON from awards struct: %w", err) 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) err = a.request("POST", fmt.Sprintf("/v1/sessions/%d/awards", a.SessionID), j, nil)
if err != nil {
return err
}
return return
} }

View File

@ -18,11 +18,13 @@ import (
var ( var (
TeamsDir string TeamsDir string
skipInitialSync bool skipInitialSync bool
dryRun bool
) )
func main() { func main() {
flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files") 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(&dryRun, "dry-run", dryRun, "Don't perform any write action, just display")
flag.BoolVar(&skipInitialSync, "skipinitialsync", skipInitialSync, "Skip the initial synchronization") 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)") 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") 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 { if v, exists := os.LookupEnv("AIRBUS_TOKEN"); exists {
api.Token = v api.Token = v
} }
if v, exists := os.LookupEnv("AIRBUS_SESSIONID"); exists { if v, exists := os.LookupEnv("AIRBUS_SESSIONID"); exists {
var err error var err error
api.SessionID, err = strconv.ParseInt(v, 10, 64) api.SessionID, err = strconv.ParseInt(v, 10, 64)
if err != nil { if err != nil {
log.Fatal("AIRBUS_SESSIONID is invalid: ", err.Error()) 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 { if v, exists := os.LookupEnv("AIRBUS_SESSIONUUID"); exists {
api.SessionUUID = v api.SessionUUID = v
} }
@ -62,12 +78,6 @@ func main() {
log.Fatal("Unable to open timestamp file: ", err.Error()) 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 // Load teams.json
teamsbindings, err := getTeams(filepath.Join(TeamsDir, "teams.json")) teamsbindings, err := getTeams(filepath.Join(TeamsDir, "teams.json"))
if err != nil { if err != nil {
@ -76,17 +86,27 @@ func main() {
w := Walker{ w := Walker{
LastSync: ts, LastSync: ts,
Exercices: exbindings,
Teams: teamsbindings, Teams: teamsbindings,
API: api, API: api,
Coeff: *coeff, 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 { if !skipInitialSync {
// Iterate over teams scores // Iterate over teams scores
err = filepath.WalkDir(TeamsDir, w.WalkScoreSync) err = filepath.WalkDir(TeamsDir, w.WalkScoreSync)
if err != nil { if err != nil {
log.Printf("Something goes wrong during walking") log.Println("Something goes wrong during walking: ", err.Error())
} }
// save current timestamp for teams // save current timestamp for teams
@ -131,6 +151,9 @@ func main() {
return return
} }
w.Teams = teamsbindings w.Teams = teamsbindings
if err = w.fetchTeams(); err != nil {
log.Fatal("Unable to fetch teams: ", err.Error())
}
// save current timestamp for teams // save current timestamp for teams
err = saveTS(*tspath, w.LastSync) err = saveTS(*tspath, w.LastSync)
@ -160,6 +183,9 @@ func main() {
return return
} }
w.Teams = teamsbindings w.Teams = teamsbindings
if err = w.fetchTeams(); err != nil {
log.Fatal("Unable to fetch teams: ", err.Error())
}
// FIXME // FIXME
@ -183,6 +209,9 @@ func main() {
return return
} }
w.Teams = teamsbindings 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 { } 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.") log.Println("FSNOTIFY WRITE SEEN. Prefer looking at them, as it appears files are not atomically moved.")

View File

@ -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
}

View File

@ -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
}
}

View File

@ -6,10 +6,15 @@ import (
"time" "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 var fd *os.File
if _, err = os.Stat(tspath); os.IsNotExist(err) { if _, err = os.Stat(tspath); os.IsNotExist(err) {
timestamp = map[AirbusUserId]time.Time{} timestamp = map[string]*TSValue{}
err = saveTS(tspath, timestamp) err = saveTS(tspath, timestamp)
return return
} else if fd, err = os.Open(tspath); err != nil { } 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 { if fd, err := os.Create(tspath); err != nil {
return err return err
} else { } else {

View File

@ -1,11 +1,11 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"time"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
@ -15,55 +15,144 @@ var (
) )
type Walker struct { type Walker struct {
LastSync map[AirbusUserId]time.Time LastSync map[string]*TSValue
Exercices AirbusExercicesBindings Exercices AirbusExercicesBindings
Teams map[string]fic.ExportedTeam Teams map[string]fic.ExportedTeam
RevTeams map[string]string
TeamBindings map[string]*AirbusTeam
API AirbusAPI API AirbusAPI
Coeff float64 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) { func (w *Walker) treat(path string) {
mypath := filepath.Join(filepath.Dir(path), "my.json") teamid := filepath.Base(filepath.Dir(path))
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()
teammy, err := fic.ReadMyJSON(fdmy) if _, ok := w.TeamBindings[teamid]; ok {
if err != nil { w.TreatScoreGrid(path, w.TeamBindings[teamid])
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
} }
} }
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 { func (w *Walker) WalkScoreSync(path string, d os.DirEntry, err error) error {
if filepath.Base(path) == "scores.json" { if filepath.Base(path) == "scores.json" {
w.treat(path) 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 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 { func (w *Walker) WalkScore(path string, d os.DirEntry, err error) error {
if filepath.Base(path) == "scores.json" { if filepath.Base(path) == "scores.json" {
go w.treat(path) go w.treat(path)
@ -71,7 +160,7 @@ func (w *Walker) WalkScore(path string, d os.DirEntry, err error) error {
return nil return nil
} }
func (w *Walker) TreatScoreGrid(path string, airbusTeamId AirbusUserId) error { func (w *Walker) TreatScoreGrid(path string, airbusTeam *AirbusTeam) error {
// Read score grid // Read score grid
fdscores, err := os.Open(path) fdscores, err := os.Open(path)
if err != nil { if err != nil {
@ -85,30 +174,35 @@ func (w *Walker) TreatScoreGrid(path string, airbusTeamId AirbusUserId) error {
} }
// Found all new entries // Found all new entries
maxts := w.LastSync[airbusTeamId] maxts := &TSValue{}
for _, row := range teamscores { if ts, ok := w.LastSync[airbusTeam.Name]; ok {
if row.Time.After(maxts) { maxts = ts
maxts = row.Time
} }
if row.Time.After(w.LastSync[airbusTeamId]) { for _, row := range teamscores {
if row.Time.After(maxts.Time) {
maxts.Time = row.Time
}
if row.Time.After(w.LastSync[airbusTeam.Name].Time) {
if !noValidateChallenge && row.Reason == "Validation" { if !noValidateChallenge && row.Reason == "Validation" {
err = w.API.ValidateChallengeFromUser(airbusTeamId, w.Exercices[row.IdExercice]) err = w.API.ValidateChallengeFromUser(airbusTeam, w.Exercices[row.IdExercice])
} else { } 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 { if err != nil {
return err return err
} }
maxts.Score += int64(row.Points * row.Coeff * w.Coeff)
} }
} }
w.LastSync[airbusTeamId] = maxts w.LastSync[airbusTeam.Name] = maxts
return nil 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 // Read current score on other platform
stats, err := w.API.GetCurrentStats() stats, err := w.API.GetCurrentStats()
if err != nil { if err != nil {
@ -120,9 +214,9 @@ func (w *Walker) BalanceScore(score int64, airbusTeamId AirbusUserId) error {
return fmt.Errorf("session not found") 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 { 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 other_score := other_team.Score
@ -130,7 +224,7 @@ func (w *Walker) BalanceScore(score int64, airbusTeamId AirbusUserId) error {
// Send diff to the platform // Send diff to the platform
if other_score != score { if other_score != score {
diff := score - other_score diff := score - other_score
return w.API.AwardUser(airbusTeamId, diff, "Équilibrage") return w.API.AwardUser(airbusTeam, diff, "Équilibrage")
} }
return nil return nil