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

View File

@ -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.")

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"
)
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 {

View File

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