From 84d0163f69ff9913e992acd7e395175c904b2e56 Mon Sep 17 00:00:00 2001 From: onestay <17146830+onestay@users.noreply.github.com> Date: Sun, 19 Dec 2021 18:27:42 +0100 Subject: [PATCH 1/4] First batch of refactoring This is the first big batch of refactoring adding some rewriting of the runs and player model as well as adding a marathon package. --- api/models/marathon.go | 10 --- api/models/player.go | 42 +++++++++++ api/models/player_test.go | 45 +++++++++++ api/models/run.go | 154 ++++++++++++++++++++++++++++++++------ api/models/run_test.go | 65 ++++++++++++++++ api/models/schedule.go | 19 +++++ marathon/marathon.go | 31 ++++++++ marathon/runState.go | 39 ++++++++++ 8 files changed, 374 insertions(+), 31 deletions(-) delete mode 100644 api/models/marathon.go create mode 100644 api/models/player.go create mode 100644 api/models/player_test.go create mode 100644 api/models/run_test.go create mode 100644 api/models/schedule.go create mode 100644 marathon/marathon.go create mode 100644 marathon/runState.go diff --git a/api/models/marathon.go b/api/models/marathon.go deleted file mode 100644 index b9bfba7..0000000 --- a/api/models/marathon.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -// Marathon represents the general Marathon -type Marathon struct { - Name string `json:"name" bson:"name"` - Runs []Run `json:"runs" bson:"runs"` - RunCount int `json:"runCount" bson:"runCount"` - CurrentRun string `json:"currentRun" bson:"currentRun"` - IsRunning bool `bson:"isRunning"` -} diff --git a/api/models/player.go b/api/models/player.go new file mode 100644 index 0000000..863939a --- /dev/null +++ b/api/models/player.go @@ -0,0 +1,42 @@ +package models + +import "database/sql" + +type PlayerInfo struct { + Id int64 `json:"id,omitempty"` + DisplayName string `json:"displayName,omitempty"` + Country string `json:"country,omitempty"` + TwitterName string `json:"twitterName,omitempty"` + TwitchName string `json:"twitchName,omitempty"` + YoutubeName string `json:"youtubeName,omitempty"` +} + +func AddPlayer(player PlayerInfo, db *sql.DB) (int64, error) { + stmt, err := db.Prepare("INSERT INTO players(display_name, country, twitter_name, twitch_name, youtube_name) VALUES (?, ?, ?, ?, ?)") + if err != nil { + return 0, err + } + defer stmt.Close() + + result, err := stmt.Exec(player.DisplayName, player.Country, player.TwitterName, player.TwitchName, player.YoutubeName) + if err != nil { + return 0, err + } + + id, err := result.LastInsertId() + if err != nil { + return 0, err + } + + return id, nil +} + +func GetPlayerById(id int64, db *sql.DB) (*PlayerInfo, error) { + var player PlayerInfo + err := db.QueryRow("SELECT * FROM players WHERE id=?", id).Scan(&player.Id, &player.DisplayName, &player.Country, &player.TwitterName, &player.TwitchName, &player.YoutubeName) + if err != nil { + return nil, err + } + + return &player, nil +} diff --git a/api/models/player_test.go b/api/models/player_test.go new file mode 100644 index 0000000..9de5d03 --- /dev/null +++ b/api/models/player_test.go @@ -0,0 +1,45 @@ +package models + +import ( + "database/sql" + "fmt" + _ "github.com/mattn/go-sqlite3" + "testing" +) + +func TestPlayer(t *testing.T) { + db, err := sql.Open("sqlite3", "../../db/test.sqlite") + if err != nil { + t.Fail() + } + + for i := 0; i < 10; i++ { + p := PlayerInfo{ + Id: 0, + DisplayName: fmt.Sprintf("onestay%d", i), + Country: "de", + TwitterName: "@onest4y", + TwitchName: "onestay", + YoutubeName: "", + } + + _, err = AddPlayer(p, db) + if err != nil { + panic(err) + } + } + + player, err := GetPlayerById(1, db) + if err != nil { + panic(err) + } + + if player.DisplayName != "onestay0" { + t.Errorf("Expected DisplayName to be %s but got %s", "onestay0", player.DisplayName) + } + + _, err = GetPlayerById(10000, db) + if err == nil { + t.Errorf("Expected GetPlayerById with id 10000 to fail") + } +} diff --git a/api/models/run.go b/api/models/run.go index a6953be..2f1d124 100644 --- a/api/models/run.go +++ b/api/models/run.go @@ -1,38 +1,150 @@ package models import ( - "gopkg.in/mgo.v2/bson" + "database/sql" + "fmt" +) + +const ( + RunIdEmptyRun = -1 + RunIdNotInit = -2 ) // Run represents a single run +// TODO: look into making more fields private here type Run struct { - RunID bson.ObjectId `json:"runID" bson:"_id"` - GameInfo GameInfo `json:"gameInfo" bson:"gameInfo"` - RunInfo runInfo `json:"runInfo" bson:"runInfo"` - Players []PlayerInfo `json:"players" bson:"playerInfo"` + Id int64 `json:"runID"` + GameInfo GameInfo `json:"gameInfo"` + RunInfo RunInfo `json:"runInfo"` + Players []PlayerInfo `json:"players"` + RunTime runTime `json:"runTime"` + PlayerRunTime map[int64]runTime `json:"playerRunTime"` } type GameInfo struct { - GameName string `json:"gameName" bson:"gameName"` - ReleaseYear int `json:"releaseYear" bson:"releaseYear"` + GameName string `json:"gameName"` + ReleaseYear int `json:"releaseYear"` +} + +type RunInfo struct { + Estimate int64 `json:"estimate"` + Category string `json:"category"` + Platform string `json:"platform"` } -type runInfo struct { - Estimate string `json:"estimate" bson:"estimate"` - Category string `json:"category" bson:"category"` - Platform string `json:"platform" bso:"platform"` +type runTime struct { + Finished bool `json:"finished"` + Time float64 `json:"time"` } -type PlayerInfo struct { - DisplayName string `json:"displayName" bson:"displayName"` - Country string `json:"country" bson:"country"` - TwitterName string `json:"twitterName" bson:"twitterName"` - TwitchName string `json:"twitchName" bson:"twitchName"` - YoutubeName string `json:"youtubeName" bson:"youtubeName"` - Timer timerPlayerInfo `json:"timer" bson:"timer"` +func CreateRun(gi GameInfo, ri RunInfo, players []PlayerInfo) Run { + var run Run + + run.GameInfo = gi + run.Players = players + run.RunInfo = ri + run.Id = RunIdNotInit + + run.RunTime = runTime{} + run.PlayerRunTime = make(map[int64]runTime, len(players)) + + return run +} + +// EmptyRun is a run identified by id 0. +func EmptyRun() *Run { + return &Run{Id: RunIdEmptyRun} +} + +func (r Run) IsEmptyRun() bool { + return r.Id == RunIdEmptyRun +} + +func (r *Run) SetID(id int64) error { + if id <= 0 { + return fmt.Errorf("invalid id %d", id) + } + + r.Id = id + + return nil } -type timerPlayerInfo struct { - Finished bool `json:"finished" bson:"finished"` - Time float64 `json:"time" bson:"time"` +func AddRun(run Run, db *sql.DB) (int64, error) { + id, err := insertRunIntoDb(&run, db) + if err != nil { + return 0, err + } + + err = run.SetID(id) + if err != nil { + return 0, err + } + + // FIXME: if this fails we probably want to delete the run from the db too since otherwise it messes with stuff + err = AppendRunToSchedule(id, db) + if err != nil { + return 0, err + } + + return id, nil +} + +//func GetRuns(db *sql.DB) []Run { +//rows, err := db.Query("SELECT * FROM run_players JOIN runs ON run_players.run_id=runs.id JOIN players ON run_players.player_id=players.id") +//} + +func insertRunIntoDb(run *Run, db *sql.DB) (int64, error) { + stmt, err := db.Prepare("INSERT INTO runs(game_name, release_year, estimate, category, platform, finished, time) VALUES (?, ?, ?, ?, ?, 0, 0)") + if err != nil { + return 0, err + } + defer stmt.Close() + + result, err := stmt.Exec(run.GameInfo.GameName, run.GameInfo.ReleaseYear, run.RunInfo.Estimate, run.RunInfo.Category, run.RunInfo.Platform) + if err != nil { + return 0, err + } + + id, err := result.LastInsertId() + if err != nil { + return 0, err + } + + err = run.SetID(id) + if err != nil { + return 0, err + } + + // FIXME: delete original entry from DB on error + err = insertRunPlayerRelation(run, db) + if err != nil { + return 0, err + } + return id, nil +} + +func insertRunPlayerRelation(run *Run, db *sql.DB) error { + tx, err := db.Begin() + if err != nil { + return err + } + stmt, err := tx.Prepare("INSERT INTO run_players (run_id, player_id) VALUES (?, ?)") + if err != nil { + return err + } + + for _, player := range run.Players { + _, err := stmt.Exec(run.Id, player.Id) + if err != nil { + // FIXME: cleanup needed + return err + } + } + err = tx.Commit() + if err != nil { + return err + } + + return nil } diff --git a/api/models/run_test.go b/api/models/run_test.go new file mode 100644 index 0000000..c622aef --- /dev/null +++ b/api/models/run_test.go @@ -0,0 +1,65 @@ +package models + +import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" + "testing" +) + +func TestNewMarathon(t *testing.T) { + gi := GameInfo{ + GameName: "Portal 3", + ReleaseYear: 2025, + } + + ri := RunInfo{ + Estimate: 30, + Category: "100%", + Platform: "PC", + } + + p := PlayerInfo{ + Id: 2, + DisplayName: "Onestay", + Country: "de", + TwitterName: "", + TwitchName: "", + YoutubeName: "", + } + + var pi []PlayerInfo + pi = append(pi, p) + + db, err := sql.Open("sqlite3", "../../db/test.sqlite") + if err != nil { + panic(err) + } + _, err = AddRun(CreateRun(gi, ri, pi), db) + if err != nil { + panic(err) + } + + gi = GameInfo{ + GameName: "Portal 4", + ReleaseYear: 2027, + } + + ri = RunInfo{ + Estimate: 30, + Category: "100%", + Platform: "PC", + } + + p = PlayerInfo{ + DisplayName: "Onestay", + Country: "de", + TwitterName: "", + TwitchName: "", + YoutubeName: "", + } + + _, err = AddRun(CreateRun(gi, ri, pi), db) + if err != nil { + panic(err) + } +} diff --git a/api/models/schedule.go b/api/models/schedule.go new file mode 100644 index 0000000..5ceba02 --- /dev/null +++ b/api/models/schedule.go @@ -0,0 +1,19 @@ +package models + +import "database/sql" + +func AppendRunToSchedule(runID int64, db *sql.DB) error { + stmt, err := db.Prepare("INSERT INTO schedule(run_id) VALUES (?)") + if err != nil { + return err + } + + defer stmt.Close() + + _, err = stmt.Exec(runID) + if err != nil { + return err + } + + return nil +} diff --git a/marathon/marathon.go b/marathon/marathon.go new file mode 100644 index 0000000..f1bb2f3 --- /dev/null +++ b/marathon/marathon.go @@ -0,0 +1,31 @@ +package marathon + +import ( + "github.com/onestay/MarathonTools-API/api/models" + "sync" +) + +const InitialRunCap = 30 + +// Marathon represents the general Marathon +type Marathon struct { + name string + runState *RunState + marathonMutex *sync.Mutex +} + +func NewMarathon(name string, index int32) *Marathon { + marathon := Marathon{ + name: name, + marathonMutex: &sync.Mutex{}, + runState: &RunState{ + index: index, + current: models.EmptyRun(), + next: models.EmptyRun(), + prev: models.EmptyRun(), + upNext: models.EmptyRun(), + }, + } + + return &marathon +} diff --git a/marathon/runState.go b/marathon/runState.go new file mode 100644 index 0000000..a7012d8 --- /dev/null +++ b/marathon/runState.go @@ -0,0 +1,39 @@ +package marathon + +import "github.com/onestay/MarathonTools-API/api/models" + +type RunState struct { + index int32 + current *models.Run + next *models.Run + prev *models.Run + upNext *models.Run +} + +func (m Marathon) GetState() RunState { + return *m.runState +} + +func (m Marathon) SetCurrentRun(run *models.Run) { + m.marathonMutex.Lock() + m.runState.current = run + m.marathonMutex.Unlock() +} + +func (m Marathon) SetNextRun(run *models.Run) { + m.marathonMutex.Lock() + m.runState.next = run + m.marathonMutex.Unlock() +} + +func (m Marathon) SetPrevRun(run *models.Run) { + m.marathonMutex.Lock() + m.runState.prev = run + m.marathonMutex.Unlock() +} + +func (m Marathon) SetUpNextRun(run *models.Run) { + m.marathonMutex.Lock() + m.runState.upNext = run + m.marathonMutex.Unlock() +} From 974cf1f7887b72572bc2c1f6c5d23e64e0ffdc0a Mon Sep 17 00:00:00 2001 From: onestay <17146830+onestay@users.noreply.github.com> Date: Sun, 19 Dec 2021 19:29:54 +0100 Subject: [PATCH 2/4] add goland config --- .idea/dataSources.xml | 12 ++++++++++++ .idea/sqldialects.xml | 8 ++++++++ 2 files changed, 20 insertions(+) create mode 100644 .idea/dataSources.xml create mode 100644 .idea/sqldialects.xml diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..c6bc382 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/db/test.sqlite + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..ed8593d --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file From 7da91030ad4139a47596fb944fd5552b23dcbc16 Mon Sep 17 00:00:00 2001 From: onestay <17146830+onestay@users.noreply.github.com> Date: Sun, 19 Dec 2021 19:31:05 +0100 Subject: [PATCH 3/4] add GetRuns function returning all runs in the DB in schedule order --- api/models/run.go | 67 ++++++++++++++++++++++++++++++++++-------- api/models/run_test.go | 44 +++++++++++++-------------- 2 files changed, 76 insertions(+), 35 deletions(-) diff --git a/api/models/run.go b/api/models/run.go index 2f1d124..313b43d 100644 --- a/api/models/run.go +++ b/api/models/run.go @@ -13,12 +13,12 @@ const ( // Run represents a single run // TODO: look into making more fields private here type Run struct { - Id int64 `json:"runID"` - GameInfo GameInfo `json:"gameInfo"` - RunInfo RunInfo `json:"runInfo"` - Players []PlayerInfo `json:"players"` - RunTime runTime `json:"runTime"` - PlayerRunTime map[int64]runTime `json:"playerRunTime"` + Id int64 `json:"runID"` + GameInfo GameInfo `json:"gameInfo"` + RunInfo RunInfo `json:"runInfo"` + Players []*PlayerInfo `json:"players"` + RunTime runTime `json:"runTime"` + PlayerRunTime map[int64]*runTime `json:"playerRunTime"` } type GameInfo struct { @@ -37,7 +37,7 @@ type runTime struct { Time float64 `json:"time"` } -func CreateRun(gi GameInfo, ri RunInfo, players []PlayerInfo) Run { +func CreateRun(gi GameInfo, ri RunInfo, players []*PlayerInfo) Run { var run Run run.GameInfo = gi @@ -46,7 +46,7 @@ func CreateRun(gi GameInfo, ri RunInfo, players []PlayerInfo) Run { run.Id = RunIdNotInit run.RunTime = runTime{} - run.PlayerRunTime = make(map[int64]runTime, len(players)) + run.PlayerRunTime = make(map[int64]*runTime, len(players)) return run } @@ -90,9 +90,50 @@ func AddRun(run Run, db *sql.DB) (int64, error) { return id, nil } -//func GetRuns(db *sql.DB) []Run { -//rows, err := db.Query("SELECT * FROM run_players JOIN runs ON run_players.run_id=runs.id JOIN players ON run_players.player_id=players.id") -//} +func GetRuns(db *sql.DB) ([]*Run, error) { + rows, err := db.Query("SELECT game_name, release_year, estimate, category, platform, r.finished, r.time, r.id, p.id, display_name, country, twitter_name, twitch_name, youtube_name, run_players.finished, run_players.time FROM run_players INNER JOIN runs r ON r.id=run_players.run_id INNER JOIN players p on p.id = run_players.player_id INNER JOIN schedule s on r.id = s.run_id ORDER BY s.pos") + if err != nil { + return nil, err + } + var runs []*Run + + var prevRunId int64 = -1 + var prevRun *Run = nil + for rows.Next() { + var gi GameInfo + var ri RunInfo + var pi PlayerInfo + var ti runTime + var playerTimeInfo runTime + var run Run + var runId int64 + err = rows.Scan(&gi.GameName, &gi.ReleaseYear, &ri.Estimate, &ri.Category, &ri.Platform, &ti.Finished, &ti.Time, &runId, &pi.Id, &pi.DisplayName, &pi.Country, &pi.TwitterName, &pi.TwitchName, &pi.YoutubeName, &playerTimeInfo.Finished, &playerTimeInfo.Time) + if err != nil { + return nil, err + } + + if prevRunId == runId { + prevRun.Players = append(prevRun.Players, &pi) + prevRun.PlayerRunTime[pi.Id] = &playerTimeInfo + } else { + run.Id = runId + run.GameInfo = gi + run.RunInfo = ri + run.Players = make([]*PlayerInfo, 1) + run.PlayerRunTime = make(map[int64]*runTime) + run.PlayerRunTime[pi.Id] = &playerTimeInfo + run.Players[0] = &pi + run.RunTime = ti + + prevRun = &run + prevRunId = runId + + runs = append(runs, &run) + } + } + + return runs, nil +} func insertRunIntoDb(run *Run, db *sql.DB) (int64, error) { stmt, err := db.Prepare("INSERT INTO runs(game_name, release_year, estimate, category, platform, finished, time) VALUES (?, ?, ?, ?, ?, 0, 0)") @@ -129,13 +170,13 @@ func insertRunPlayerRelation(run *Run, db *sql.DB) error { if err != nil { return err } - stmt, err := tx.Prepare("INSERT INTO run_players (run_id, player_id) VALUES (?, ?)") + stmt, err := tx.Prepare("INSERT INTO run_players (run_id, player_id, finished, time) VALUES (?, ?, ?, ?)") if err != nil { return err } for _, player := range run.Players { - _, err := stmt.Exec(run.Id, player.Id) + _, err := stmt.Exec(run.Id, player.Id, false, 0) if err != nil { // FIXME: cleanup needed return err diff --git a/api/models/run_test.go b/api/models/run_test.go index c622aef..c453e1f 100644 --- a/api/models/run_test.go +++ b/api/models/run_test.go @@ -7,6 +7,11 @@ import ( ) func TestNewMarathon(t *testing.T) { + db, err := sql.Open("sqlite3", "../../db/test.sqlite") + if err != nil { + panic(err) + } + gi := GameInfo{ GameName: "Portal 3", ReleaseYear: 2025, @@ -18,22 +23,14 @@ func TestNewMarathon(t *testing.T) { Platform: "PC", } - p := PlayerInfo{ - Id: 2, - DisplayName: "Onestay", - Country: "de", - TwitterName: "", - TwitchName: "", - YoutubeName: "", - } + p1, err := GetPlayerById(1, db) + p2, err := GetPlayerById(2, db) + p3, err := GetPlayerById(3, db) - var pi []PlayerInfo - pi = append(pi, p) + var pi []*PlayerInfo + pi = append(pi, p1) + pi = append(pi, p2) - db, err := sql.Open("sqlite3", "../../db/test.sqlite") - if err != nil { - panic(err) - } _, err = AddRun(CreateRun(gi, ri, pi), db) if err != nil { panic(err) @@ -50,16 +47,19 @@ func TestNewMarathon(t *testing.T) { Platform: "PC", } - p = PlayerInfo{ - DisplayName: "Onestay", - Country: "de", - TwitterName: "", - TwitchName: "", - YoutubeName: "", + var pi2 []*PlayerInfo + pi2 = append(pi2, p3) + _, err = AddRun(CreateRun(gi, ri, pi2), db) + if err != nil { + panic(err) } - _, err = AddRun(CreateRun(gi, ri, pi), db) + runs, err := GetRuns(db) if err != nil { - panic(err) + t.Errorf("Error %v", err) + } + + if len(runs) == 0 { + t.Errorf("invalid run length") } } From a164462c52ffc87d17c8857624f2cafe07f83f13 Mon Sep 17 00:00:00 2001 From: onestay <17146830+onestay@users.noreply.github.com> Date: Mon, 20 Dec 2021 01:35:56 +0100 Subject: [PATCH 4/4] add some more stuff --- api/common/base.go | 26 +++---- api/models/run.go | 164 ++++++++++++++++++++++++++++++++++------ api/models/run_test.go | 74 +++++++++--------- api/routes/runs/runs.go | 14 ++-- go.mod | 5 +- go.sum | 13 ++-- main.go | 27 +++++-- 7 files changed, 221 insertions(+), 102 deletions(-) diff --git a/api/common/base.go b/api/common/base.go index 5f56ac3..70f1d1e 100644 --- a/api/common/base.go +++ b/api/common/base.go @@ -1,33 +1,29 @@ package common import ( + "database/sql" + "github.com/onestay/MarathonTools-API/marathon" "net/http" "github.com/go-redis/redis" "gopkg.in/mgo.v2" - "github.com/onestay/MarathonTools-API/api/models" "github.com/onestay/MarathonTools-API/ws" ) // Controller is the base struct for any controller. It's used to manage state and other things. type Controller struct { WS *ws.Hub - MGS *mgo.Session - Col *mgo.Collection - RunIndex int - CurrentRun *models.Run - NextRun *models.Run - PrevRun *models.Run - UpNext *models.Run RedisClient *redis.Client TimerState TimerState TimerTime float64 HTTPClient http.Client + Marathon marathon.Marathon // SocialUpdatesChan is used to communicate with the socialController on Twitter and twitch updates SocialUpdatesChan chan int CL *Checklist Settings *SettingsProvider + db *sql.DB } type httpResponse struct { @@ -55,26 +51,24 @@ const ( ) // NewController returns a new base controller -func NewController(hub *ws.Hub, mgs *mgo.Session, crIndex int, rc *redis.Client) *Controller { - var runs []models.Run - err := mgs.DB("marathon").C("runs").Find(nil).All(&runs) +func NewController(hub *ws.Hub, mgs *mgo.Session, crIndex int, rc *redis.Client) (*Controller, error) { + db, err := sql.Open("sqlite3", "../../db/test.sqlite") if err != nil { - return nil + return nil, err } + c := &Controller{ WS: hub, - MGS: mgs, - RunIndex: crIndex, - Col: mgs.DB("marathon").C("runs"), RedisClient: rc, TimerState: 2, TimerTime: 0, HTTPClient: http.Client{}, SocialUpdatesChan: make(chan int, 1), + db: db, } c.CL = NewChecklist(c) c.Settings = InitSettings(c) c.UpdateActiveRuns() c.UpdateUpNext() - return c + return c, nil } diff --git a/api/models/run.go b/api/models/run.go index 313b43d..59f67ad 100644 --- a/api/models/run.go +++ b/api/models/run.go @@ -90,8 +90,68 @@ func AddRun(run Run, db *sql.DB) (int64, error) { return id, nil } +func insertRunIntoDb(run *Run, db *sql.DB) (int64, error) { + stmt, err := db.Prepare("INSERT INTO runs(game_name, release_year, estimate, category, platform, finished, time) VALUES (?, ?, ?, ?, ?, 0, 0)") + if err != nil { + return 0, err + } + defer stmt.Close() + + result, err := stmt.Exec(run.GameInfo.GameName, run.GameInfo.ReleaseYear, run.RunInfo.Estimate, run.RunInfo.Category, run.RunInfo.Platform) + if err != nil { + return 0, err + } + + id, err := result.LastInsertId() + if err != nil { + return 0, err + } + + err = run.SetID(id) + if err != nil { + return 0, err + } + + // FIXME: delete original entry from DB on error + err = insertRunPlayerRelation(run, db) + if err != nil { + return 0, err + } + return id, nil +} + +func insertRunPlayerRelation(run *Run, db *sql.DB) error { + tx, err := db.Begin() + if err != nil { + return err + } + stmt, err := tx.Prepare("INSERT INTO run_players (run_id, player_id, finished, time) VALUES (?, ?, ?, ?)") + if err != nil { + return err + } + + for _, player := range run.Players { + _, err := stmt.Exec(run.Id, player.Id, false, 0) + if err != nil { + // FIXME: cleanup needed + return err + } + } + err = tx.Commit() + if err != nil { + return err + } + + return nil +} + func GetRuns(db *sql.DB) ([]*Run, error) { - rows, err := db.Query("SELECT game_name, release_year, estimate, category, platform, r.finished, r.time, r.id, p.id, display_name, country, twitter_name, twitch_name, youtube_name, run_players.finished, run_players.time FROM run_players INNER JOIN runs r ON r.id=run_players.run_id INNER JOIN players p on p.id = run_players.player_id INNER JOIN schedule s on r.id = s.run_id ORDER BY s.pos") + sqlStmt := ` SELECT game_name, release_year, estimate, category, platform, r.finished, r.time, r.id, p.id, display_name, country, twitter_name, twitch_name, youtube_name, run_players.finished, run_players.time FROM run_players + INNER JOIN runs r ON r.id=run_players.run_id + INNER JOIN players p on p.id = run_players.player_id + INNER JOIN schedule s on r.id = s.run_id + ORDER BY row_number() OVER ()` + rows, err := db.Query(sqlStmt) if err != nil { return nil, err } @@ -135,54 +195,108 @@ func GetRuns(db *sql.DB) ([]*Run, error) { return runs, nil } -func insertRunIntoDb(run *Run, db *sql.DB) (int64, error) { - stmt, err := db.Prepare("INSERT INTO runs(game_name, release_year, estimate, category, platform, finished, time) VALUES (?, ?, ?, ?, ?, 0, 0)") +func getRunByRunID(runId int64, db *sql.DB) (*Run, error) { + sqlStmt := ` SELECT game_name, release_year, estimate, category, platform, r.finished, r.time, r.id, p.id, display_name, country, twitter_name, twitch_name, youtube_name, run_players.finished, run_players.time FROM run_players + INNER JOIN runs r ON r.id=run_players.run_id + INNER JOIN players p on p.id = run_players.player_id + WHERE r.id=?` + rows, err := db.Query(sqlStmt, runId) if err != nil { - return 0, err + return nil, err } - defer stmt.Close() - result, err := stmt.Exec(run.GameInfo.GameName, run.GameInfo.ReleaseYear, run.RunInfo.Estimate, run.RunInfo.Category, run.RunInfo.Platform) + var run *Run = nil + for rows.Next() { + var gi GameInfo + var ri RunInfo + var pi PlayerInfo + var ti runTime + var playerTimeInfo runTime + var currentRun Run + var runId int64 + err = rows.Scan(&gi.GameName, &gi.ReleaseYear, &ri.Estimate, &ri.Category, &ri.Platform, &ti.Finished, &ti.Time, &runId, &pi.Id, &pi.DisplayName, &pi.Country, &pi.TwitterName, &pi.TwitchName, &pi.YoutubeName, &playerTimeInfo.Finished, &playerTimeInfo.Time) + if err != nil { + return nil, err + } + + if run != nil { + run.Players = append(currentRun.Players, &pi) + run.PlayerRunTime[pi.Id] = &playerTimeInfo + } else { + currentRun.Id = runId + currentRun.GameInfo = gi + currentRun.RunInfo = ri + currentRun.Players = make([]*PlayerInfo, 1) + currentRun.PlayerRunTime = make(map[int64]*runTime) + currentRun.PlayerRunTime[pi.Id] = &playerTimeInfo + currentRun.Players[0] = &pi + currentRun.RunTime = ti + + run = ¤tRun + } + } + + return run, nil + +} + +func getSchedulePositionFromRunId(runId int64, db *sql.DB) (int64, error) { + if runId < 0 { + return 0, fmt.Errorf("invalid runId provided") + } + sqlStmt := `SELECT schedule.pos FROM schedule WHERE schedule.run_id == ?` + + var currentPos int64 + err := db.QueryRow(sqlStmt, runId).Scan(¤tPos) if err != nil { return 0, err } - id, err := result.LastInsertId() + return currentPos, nil +} + +func getRunBySchedulePosition(position int64, db *sql.DB) (*Run, error) { + sqlStmt := `SELECT schedule.run_id FROM schedule WHERE pos=?` + + var runId int64 + err := db.QueryRow(sqlStmt, position).Scan(&runId) if err != nil { - return 0, err + return nil, err } - err = run.SetID(id) + return getRunByRunID(position, db) +} + +func GetNextRun(runId int64, db *sql.DB) (*Run, error) { + currentPos, err := getSchedulePositionFromRunId(runId, db) if err != nil { - return 0, err + return nil, err } - // FIXME: delete original entry from DB on error - err = insertRunPlayerRelation(run, db) + return getRunBySchedulePosition(currentPos+1, db) +} + +func GetPrevRun(runId int64, db *sql.DB) (*Run, error) { + currentPos, err := getSchedulePositionFromRunId(runId, db) if err != nil { - return 0, err + return nil, err } - return id, nil + + return getRunBySchedulePosition(currentPos-1, db) } -func insertRunPlayerRelation(run *Run, db *sql.DB) error { - tx, err := db.Begin() +func DeleteRun(runId int64, db *sql.DB) error { + _, err := db.Exec("DELETE FROM schedule WHERE run_id=?", runId) if err != nil { return err } - stmt, err := tx.Prepare("INSERT INTO run_players (run_id, player_id, finished, time) VALUES (?, ?, ?, ?)") + + _, err = db.Exec("DELETE FROM run_players WHERE run_id=?", runId) if err != nil { return err } - for _, player := range run.Players { - _, err := stmt.Exec(run.Id, player.Id, false, 0) - if err != nil { - // FIXME: cleanup needed - return err - } - } - err = tx.Commit() + _, err = db.Exec("DELETE FROM runs WHERE id=?", runId) if err != nil { return err } diff --git a/api/models/run_test.go b/api/models/run_test.go index c453e1f..fef5cee 100644 --- a/api/models/run_test.go +++ b/api/models/run_test.go @@ -6,60 +6,56 @@ import ( "testing" ) -func TestNewMarathon(t *testing.T) { +func TestAddRun(t *testing.T) { db, err := sql.Open("sqlite3", "../../db/test.sqlite") if err != nil { panic(err) } - gi := GameInfo{ - GameName: "Portal 3", - ReleaseYear: 2025, - } - - ri := RunInfo{ - Estimate: 30, - Category: "100%", - Platform: "PC", - } + gi1 := GameInfo{GameName: "Portal 1"} + gi2 := GameInfo{GameName: "Portal 2"} + gi3 := GameInfo{GameName: "Portal 3"} + gi4 := GameInfo{GameName: "Portal 4"} + gi5 := GameInfo{GameName: "Portal 5"} + + p1, _ := GetPlayerById(1, db) + p2, _ := GetPlayerById(2, db) + p3, _ := GetPlayerById(3, db) + p4, _ := GetPlayerById(4, db) + p5, _ := GetPlayerById(5, db) + p6, _ := GetPlayerById(6, db) + p7, _ := GetPlayerById(7, db) + p8, _ := GetPlayerById(8, db) + p9, _ := GetPlayerById(9, db) + + var pi1 = []*PlayerInfo{p1} + var pi2 = []*PlayerInfo{p2, p6} + var pi3 = []*PlayerInfo{p4, p5} + var pi4 = []*PlayerInfo{p8, p1, p9, p3} + var pi5 = []*PlayerInfo{p7} + + AddRun(CreateRun(gi1, RunInfo{}, pi1), db) + AddRun(CreateRun(gi2, RunInfo{}, pi2), db) + AddRun(CreateRun(gi3, RunInfo{}, pi3), db) + AddRun(CreateRun(gi4, RunInfo{}, pi4), db) + AddRun(CreateRun(gi5, RunInfo{}, pi5), db) - p1, err := GetPlayerById(1, db) - p2, err := GetPlayerById(2, db) - p3, err := GetPlayerById(3, db) - - var pi []*PlayerInfo - pi = append(pi, p1) - pi = append(pi, p2) - - _, err = AddRun(CreateRun(gi, ri, pi), db) + runs, err := GetRuns(db) if err != nil { - panic(err) - } - - gi = GameInfo{ - GameName: "Portal 4", - ReleaseYear: 2027, + t.Fatalf("Error %v", err) } - ri = RunInfo{ - Estimate: 30, - Category: "100%", - Platform: "PC", + if len(runs) != 5 { + t.Fatalf("invalid run length expected 5 but got %d", len(runs)) } - var pi2 []*PlayerInfo - pi2 = append(pi2, p3) - _, err = AddRun(CreateRun(gi, ri, pi2), db) + aRun, err := getRunBySchedulePosition(1, db) if err != nil { panic(err) } - runs, err := GetRuns(db) - if err != nil { - t.Errorf("Error %v", err) + if aRun.GameInfo.GameName != "Portal 1" { + t.Fatalf("Expected game name at schedule position 1 to be \"Portal3\" but got %s", aRun.GameInfo.GameName) } - if len(runs) == 0 { - t.Errorf("invalid run length") - } } diff --git a/api/routes/runs/runs.go b/api/routes/runs/runs.go index c2c98f9..757a982 100644 --- a/api/routes/runs/runs.go +++ b/api/routes/runs/runs.go @@ -57,7 +57,7 @@ func (rc RunController) AddRun(w http.ResponseWriter, r *http.Request, _ httprou run := models.Run{} json.NewDecoder(r.Body).Decode(&run) - run.RunID = bson.NewObjectId() + run.id = bson.NewObjectId() err := rc.base.MGS.DB("marathon").C("runs").Insert(run) if err != nil { @@ -65,7 +65,7 @@ func (rc RunController) AddRun(w http.ResponseWriter, r *http.Request, _ httprou return } - rc.base.Response(run.RunID.Hex(), "", http.StatusOK, w) + rc.base.Response(run.id.Hex(), "", http.StatusOK, w) go rc.base.WSRunsOnlyUpdate() go rc.base.UpdateActiveRuns() @@ -158,7 +158,7 @@ func (rc RunController) UpdateRun(w http.ResponseWriter, r *http.Request, ps htt log.Printf("Error in UpdateRun: %v", err) return } - updatedRun.RunID = bson.ObjectIdHex(runID) + updatedRun.id = bson.ObjectIdHex(runID) err = rc.base.MGS.DB("marathon").C("runs").UpdateId(bson.ObjectIdHex(runID), updatedRun) if err != nil { @@ -169,7 +169,7 @@ func (rc RunController) UpdateRun(w http.ResponseWriter, r *http.Request, ps htt w.WriteHeader(http.StatusNoContent) rc.base.WSRunsOnlyUpdate() - if updatedRun.RunID == rc.base.CurrentRun.RunID { + if updatedRun.id == rc.base.CurrentRun.id { rc.base.CurrentRun = &updatedRun rc.base.WSCurrentUpdate() } @@ -201,9 +201,9 @@ func (rc RunController) MoveRun(w http.ResponseWriter, _ *http.Request, ps httpr var indexToInsert int for i := 0; i < len(runs); i++ { - if runs[i].RunID == bson.ObjectIdHex(runID) { + if runs[i].id == bson.ObjectIdHex(runID) { index = i - } else if runs[i].RunID == bson.ObjectIdHex(after) { + } else if runs[i].id == bson.ObjectIdHex(after) { indexToInsert = i } } @@ -275,7 +275,7 @@ func (rc *RunController) UploadRunJSON(w http.ResponseWriter, r *http.Request, _ rc.base.MGS.DB("marathon").C("runs").RemoveAll(nil) for _, run := range runs { - run.RunID = bson.NewObjectId() + run.id = bson.NewObjectId() err := rc.base.MGS.DB("marathon").C("runs").Insert(run) if err != nil { panic("error adding run from UploadRunJSON into db") diff --git a/go.mod b/go.mod index aad8fbb..4a569b2 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,19 @@ module github.com/onestay/MarathonTools-API go 1.17 require ( - github.com/PuerkitoBio/goquery v1.7.1 + github.com/PuerkitoBio/goquery v1.8.0 github.com/dghubble/oauth1 v0.7.0 github.com/go-redis/redis v6.15.9+incompatible github.com/gorilla/websocket v1.4.2 github.com/joho/godotenv v1.4.0 github.com/julienschmidt/httprouter v1.3.0 - golang.org/x/net v0.0.0-20211008194852-3b03d305991f + golang.org/x/net v0.0.0-20211216030914-fe4d6282115f gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 ) require ( github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/mattn/go-sqlite3 v1.14.9 // indirect github.com/onsi/ginkgo v1.16.4 // indirect github.com/onsi/gomega v1.16.0 // indirect github.com/stretchr/testify v1.7.0 // indirect diff --git a/go.sum b/go.sum index 8e8afd1..41999d7 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ -github.com/PuerkitoBio/goquery v1.7.1 h1:oE+T06D+1T7LNrn91B4aERsRIeCLJ/oPSa6xB9FPnz4= -github.com/PuerkitoBio/goquery v1.7.1/go.mod h1:XY0pP4kfraEmmV1O7Uf6XyjoslwsneBbgeDjLYuN8xY= -github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -39,6 +38,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -62,17 +63,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211008194852-3b03d305991f h1:1scJEYZBaF48BaG6tYbtxmLcXqwYGSfGcMoStTqkkIw= -golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/main.go b/main.go index ca17345..a69122c 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "database/sql" "log" "net/http" "os" @@ -51,6 +52,7 @@ func init() { log.Println("Error loading .env file.") } parseEnvVars() + openDB("./db") log.Printf("Connecting to mgo server at %v", mgoURL) mgs = getMongoSession() log.Printf("Connecting to redis server at %v", redisURL) @@ -65,7 +67,11 @@ func startHTTPServer() { r := httprouter.New() hub := ws.NewHub() log.Println("Initializing base controller...") - baseController := common.NewController(hub, mgs, 0, redisClient) + baseController, err := common.NewController(hub, mgs, 0, redisClient) + if err != nil { + log.Fatalf("Error initializing base controller %v", err) + + } log.Println("Initializing social controller...") social.NewSocialController(twitchClientID, twitchClientSecret, twitchCallback, twitterKey, twitterSecret, twitterCallback, socialAuthURL, socialAuthKey, featuredChannelsKey, baseController, r) log.Println("Initializing time controller...") @@ -75,19 +81,19 @@ func startHTTPServer() { var donProv donations.DonationProvider donationsEnabled := true - var err error + var donationProviderError error if os.Getenv("DONATION_PROVIDER") == "gdq" { log.Println("Creating new GDQ donation provider") - donProv, err = donationProviders.NewGDQDonationProvider(gdqURL, gdqEventID, gdqUsername, gdqPassword) - if err != nil { + donProv, donationProviderError = donationProviders.NewGDQDonationProvider(gdqURL, gdqEventID, gdqUsername, gdqPassword) + if donationProviderError != nil { log.Printf("Error during gdq donation provider creation: %v", err) donationsEnabled = false } } else if os.Getenv("DONATION_PROVIDER") == "srcom" { log.Println("Creating new speedrun.com donation provider") - donProv, err = donationProviders.NewSRComDonationProvider(marathonSlug) - if err != nil { + donProv, donationProviderError = donationProviders.NewSRComDonationProvider(marathonSlug) + if donationProviderError != nil { log.Printf("Error during donation provider creation: %v", err) donationsEnabled = false } @@ -136,6 +142,15 @@ func getMongoSession() *mgo.Session { return s } +func openDB(path string) (*sql.DB, error) { + db, err := sql.Open("sqlite3", "./db/test.db") + if err != nil { + return nil, err + } + + return db, nil +} + func getRedisClient() *redis.Client { client := redis.NewClient(&redis.Options{ Addr: redisURL + ":6379",