ws and pruner done!!!
This commit is contained in:
parent
c5cc245e25
commit
74add408e6
34 changed files with 1455 additions and 221 deletions
2
.envrc
2
.envrc
|
@ -1,2 +1,2 @@
|
||||||
source .env;
|
dotenv;
|
||||||
use flake;
|
use flake;
|
||||||
|
|
52
cmd/pruner/pruner.go
Normal file
52
cmd/pruner/pruner.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/store"
|
||||||
|
"github.com/genudine/saerro-go/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db, err := util.GetDBConnection(os.Getenv("DB_ADDR"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
playerStore := store.NewPlayerStore(db)
|
||||||
|
i, err := playerStore.Prune(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("pruner: playerStore.Prune failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("pruner: deleted %d players", i)
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
vehicleStore := store.NewVehicleStore(db)
|
||||||
|
i, err := vehicleStore.Prune(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("pruner: vehicleStore.Prune failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("pruner: deleted %d vehicles", i)
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
|
@ -1,72 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/genudine/saerro-go/translators"
|
|
||||||
"github.com/genudine/saerro-go/types"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getEventHandlerTestShim(t *testing.T) (EventHandler, *sql.DB) {
|
|
||||||
db, err := sql.Open("sqlite", ":memory:")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test shim: sqlite open failed, %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return EventHandler{
|
|
||||||
Ingest: &Ingest{
|
|
||||||
DB: db,
|
|
||||||
},
|
|
||||||
}, db
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleDeath(t *testing.T) {
|
|
||||||
eh, db := getEventHandlerTestShim(t)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
event := types.ESSEvent{
|
|
||||||
EventName: "Death",
|
|
||||||
WorldID: 17,
|
|
||||||
ZoneID: 2,
|
|
||||||
|
|
||||||
CharacterID: "DollNC",
|
|
||||||
LoadoutID: "3",
|
|
||||||
TeamID: types.NC,
|
|
||||||
|
|
||||||
AttackerCharacterID: "Lyyti",
|
|
||||||
AttackerLoadoutID: "3",
|
|
||||||
AttackerTeamID: types.TR,
|
|
||||||
}
|
|
||||||
|
|
||||||
eh.HandleDeath(ctx, event)
|
|
||||||
|
|
||||||
type player struct {
|
|
||||||
CharacterID string `json:"character_id"`
|
|
||||||
ClassName string `json:"class_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var player1 player
|
|
||||||
err := db.QueryRowContext(ctx, "SELECT * FROM players WHERE character_id = ?", event.CharacterID).Scan(&player1)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, event.CharacterID, player1.CharacterID)
|
|
||||||
assert.Equal(t, translators.ClassFromLoadout(event.LoadoutID), player1.ClassName)
|
|
||||||
|
|
||||||
var player2 player
|
|
||||||
err = db.QueryRowContext(ctx, "SELECT * FROM players WHERE character_id = ?", event.AttackerCharacterID).Scan(&player2)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, event.AttackerCharacterID, player2.CharacterID)
|
|
||||||
assert.Equal(t, translators.ClassFromLoadout(event.AttackerLoadoutID), player2.ClassName)
|
|
||||||
}
|
|
|
@ -1,19 +1,30 @@
|
||||||
package main
|
package eventhandler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/cmd/ws/ingest"
|
||||||
|
"github.com/genudine/saerro-go/store"
|
||||||
"github.com/genudine/saerro-go/types"
|
"github.com/genudine/saerro-go/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventHandler struct {
|
type EventHandler struct {
|
||||||
Ingest *Ingest
|
Ingest *ingest.Ingest
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEventHandler(db *sql.DB) EventHandler {
|
||||||
|
return EventHandler{
|
||||||
|
Ingest: &ingest.Ingest{
|
||||||
|
PlayerStore: store.NewPlayerStore(db),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (eh *EventHandler) HandleEvent(ctx context.Context, event types.ESSEvent) {
|
func (eh *EventHandler) HandleEvent(ctx context.Context, event types.ESSEvent) {
|
||||||
if event.EventName == "" {
|
if event.EventName == "" {
|
||||||
log.Println("invalid event; dropping")
|
// log.Println("invalid event; dropping")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.EventName == "Death" || event.EventName == "VehicleDestroy" {
|
if event.EventName == "Death" || event.EventName == "VehicleDestroy" {
|
||||||
|
@ -27,14 +38,14 @@ func (eh *EventHandler) HandleEvent(ctx context.Context, event types.ESSEvent) {
|
||||||
|
|
||||||
func (eh *EventHandler) HandleDeath(ctx context.Context, event types.ESSEvent) {
|
func (eh *EventHandler) HandleDeath(ctx context.Context, event types.ESSEvent) {
|
||||||
if event.CharacterID != "" && event.CharacterID != "0" {
|
if event.CharacterID != "" && event.CharacterID != "0" {
|
||||||
log.Println("got pop event")
|
// log.Println("got pop event")
|
||||||
pe := PopEventFromESSEvent(event, false)
|
pe := types.PopEventFromESSEvent(event, false)
|
||||||
eh.Ingest.TrackPop(ctx, pe)
|
eh.Ingest.TrackPop(ctx, pe)
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.AttackerCharacterID != "" && event.AttackerCharacterID != "0" && event.AttackerTeamID != 0 {
|
if event.AttackerCharacterID != "" && event.AttackerCharacterID != "0" && event.AttackerTeamID != 0 {
|
||||||
log.Println("got attacker pop event")
|
pe := types.PopEventFromESSEvent(event, true)
|
||||||
pe := PopEventFromESSEvent(event, true)
|
// fmt.Println("got attacker pop event", event)
|
||||||
eh.Ingest.TrackPop(ctx, pe)
|
eh.Ingest.TrackPop(ctx, pe)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,18 +56,16 @@ func (eh *EventHandler) HandleExperience(ctx context.Context, event types.ESSEve
|
||||||
switch event.ExperienceID {
|
switch event.ExperienceID {
|
||||||
case 201: // Galaxy Spawn Bonus
|
case 201: // Galaxy Spawn Bonus
|
||||||
vehicleID = "11"
|
vehicleID = "11"
|
||||||
break
|
|
||||||
case 233: // Sunderer Spawn Bonus
|
case 233: // Sunderer Spawn Bonus
|
||||||
vehicleID = "2"
|
vehicleID = "2"
|
||||||
break
|
case 674:
|
||||||
case 674: // ANT stuff
|
fallthrough // ANT stuff
|
||||||
case 675:
|
case 675:
|
||||||
vehicleID = "160"
|
vehicleID = "160"
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
event.VehicleID = vehicleID
|
event.VehicleID = vehicleID
|
||||||
pe := PopEventFromESSEvent(event, false)
|
pe := types.PopEventFromESSEvent(event, false)
|
||||||
eh.Ingest.TrackPop(ctx, pe)
|
eh.Ingest.TrackPop(ctx, pe)
|
||||||
}
|
}
|
||||||
|
|
144
cmd/ws/eventhandler/event_handler_test.go
Normal file
144
cmd/ws/eventhandler/event_handler_test.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
package eventhandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/avast/retry-go"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/cmd/ws/ingest"
|
||||||
|
"github.com/genudine/saerro-go/store"
|
||||||
|
"github.com/genudine/saerro-go/translators"
|
||||||
|
"github.com/genudine/saerro-go/types"
|
||||||
|
"github.com/genudine/saerro-go/util/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getEventHandlerTestShim(t *testing.T) (EventHandler, context.Context) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
db := testutil.GetTestDB(t)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
return EventHandler{
|
||||||
|
Ingest: &ingest.Ingest{
|
||||||
|
PlayerStore: store.NewPlayerStore(db),
|
||||||
|
},
|
||||||
|
}, ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleDeath(t *testing.T) {
|
||||||
|
eh, ctx := getEventHandlerTestShim(t)
|
||||||
|
|
||||||
|
event := types.ESSEvent{
|
||||||
|
EventName: "Death",
|
||||||
|
WorldID: 17,
|
||||||
|
ZoneID: 2,
|
||||||
|
|
||||||
|
CharacterID: "LyytisDoll",
|
||||||
|
LoadoutID: 3,
|
||||||
|
TeamID: types.NC,
|
||||||
|
|
||||||
|
AttackerCharacterID: "Lyyti",
|
||||||
|
AttackerLoadoutID: 3,
|
||||||
|
AttackerTeamID: types.TR,
|
||||||
|
}
|
||||||
|
|
||||||
|
eh.HandleDeath(ctx, event)
|
||||||
|
|
||||||
|
player1, err := eh.Ingest.PlayerStore.GetOne(ctx, event.CharacterID)
|
||||||
|
assert.NoError(t, err, "player1 fetch failed")
|
||||||
|
assert.Equal(t, event.CharacterID, player1.CharacterID)
|
||||||
|
assert.Equal(t, string(translators.ClassFromLoadout(event.LoadoutID)), player1.ClassName)
|
||||||
|
|
||||||
|
player2, err := eh.Ingest.PlayerStore.GetOne(ctx, event.AttackerCharacterID)
|
||||||
|
assert.NoError(t, err, "player2 fetch failed")
|
||||||
|
assert.Equal(t, event.AttackerCharacterID, player2.CharacterID)
|
||||||
|
assert.Equal(t, string(translators.ClassFromLoadout(event.AttackerLoadoutID)), player2.ClassName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleExperience(t *testing.T) {
|
||||||
|
eh, ctx := getEventHandlerTestShim(t)
|
||||||
|
|
||||||
|
event := types.ESSEvent{
|
||||||
|
EventName: "GainExperience",
|
||||||
|
WorldID: 17,
|
||||||
|
ZoneID: 2,
|
||||||
|
|
||||||
|
CharacterID: "LyytisDoll",
|
||||||
|
LoadoutID: 3,
|
||||||
|
TeamID: types.NC,
|
||||||
|
|
||||||
|
ExperienceID: 674,
|
||||||
|
}
|
||||||
|
|
||||||
|
eh.HandleExperience(ctx, event)
|
||||||
|
player, err := eh.Ingest.PlayerStore.GetOne(ctx, event.CharacterID)
|
||||||
|
assert.NoError(t, err, "player fetch check failed")
|
||||||
|
assert.Equal(t, event.CharacterID, player.CharacterID)
|
||||||
|
assert.Equal(t, string(translators.ClassFromLoadout(event.LoadoutID)), player.ClassName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleAnalytics(t *testing.T) {
|
||||||
|
eh, ctx := getEventHandlerTestShim(t)
|
||||||
|
event := types.ESSEvent{
|
||||||
|
EventName: "GainExperience",
|
||||||
|
WorldID: 17,
|
||||||
|
ZoneID: 2,
|
||||||
|
|
||||||
|
CharacterID: "LyytisDoll",
|
||||||
|
LoadoutID: 3,
|
||||||
|
TeamID: types.NC,
|
||||||
|
|
||||||
|
ExperienceID: 674,
|
||||||
|
}
|
||||||
|
|
||||||
|
eh.HandleAnalytics(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleEvent(t *testing.T) {
|
||||||
|
eh, ctx := getEventHandlerTestShim(t)
|
||||||
|
|
||||||
|
events := []types.ESSEvent{
|
||||||
|
{
|
||||||
|
EventName: "Death",
|
||||||
|
WorldID: 17,
|
||||||
|
ZoneID: 2,
|
||||||
|
|
||||||
|
CharacterID: "LyytisDoll",
|
||||||
|
LoadoutID: 3,
|
||||||
|
TeamID: types.NC,
|
||||||
|
|
||||||
|
AttackerCharacterID: "Lyyti",
|
||||||
|
AttackerLoadoutID: 3,
|
||||||
|
AttackerTeamID: types.TR,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EventName: "GainExperience",
|
||||||
|
WorldID: 17,
|
||||||
|
ZoneID: 2,
|
||||||
|
|
||||||
|
CharacterID: "DollNC",
|
||||||
|
LoadoutID: 3,
|
||||||
|
TeamID: types.NC,
|
||||||
|
|
||||||
|
ExperienceID: 201,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range events {
|
||||||
|
eh.HandleEvent(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPlayers := []string{"LyytisDoll", "Lyyti", "DollNC"}
|
||||||
|
for _, id := range checkPlayers {
|
||||||
|
// eventual consistency <333
|
||||||
|
err := retry.Do(func() error {
|
||||||
|
_, err := eh.Ingest.PlayerStore.GetOne(ctx, id)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Ingest struct {
|
|
||||||
DB *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Ingest) TrackPop(ctx context.Context, event PopEvent) {
|
|
||||||
|
|
||||||
}
|
|
55
cmd/ws/ingest/ingest.go
Normal file
55
cmd/ws/ingest/ingest.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package ingest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/store"
|
||||||
|
"github.com/genudine/saerro-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Ingest struct {
|
||||||
|
PlayerStore store.IPlayerStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Ingest) TrackPop(ctx context.Context, event types.PopEvent) {
|
||||||
|
player := event.ToPlayer()
|
||||||
|
|
||||||
|
err := i.fixupPlayer(ctx, player)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ingest: player fixup failed, dropping event", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.PlayerStore.Insert(ctx, player)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("TrackPop Insert failed", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Ingest) fixupPlayer(ctx context.Context, player *types.Player) error {
|
||||||
|
if player.ClassName != "unknown" && player.FactionID != 0 {
|
||||||
|
// all fixups are done
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
storedPlayer, err := i.PlayerStore.GetOne(ctx, player.CharacterID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ingest: fixupPlayer: fetching player %s failed: %w", player.CharacterID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// probably VehicleDestroy
|
||||||
|
if player.ClassName == "unknown" {
|
||||||
|
// TODO: maybe get this from census, profile_id
|
||||||
|
player.ClassName = storedPlayer.ClassName
|
||||||
|
}
|
||||||
|
|
||||||
|
// probably PS4
|
||||||
|
if player.FactionID == 0 {
|
||||||
|
// TODO: get this from census
|
||||||
|
player.FactionID = storedPlayer.FactionID
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
84
cmd/ws/ingest/ingest_test.go
Normal file
84
cmd/ws/ingest/ingest_test.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package ingest_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/cmd/ws/ingest"
|
||||||
|
"github.com/genudine/saerro-go/store/storemock"
|
||||||
|
"github.com/genudine/saerro-go/translators"
|
||||||
|
"github.com/genudine/saerro-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mkIngest(t *testing.T) (context.Context, *ingest.Ingest, *storemock.MockPlayerStore) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ps := new(storemock.MockPlayerStore)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
i := &ingest.Ingest{
|
||||||
|
PlayerStore: ps,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, i, ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrackPopHappyPath(t *testing.T) {
|
||||||
|
ctx, i, ps := mkIngest(t)
|
||||||
|
|
||||||
|
// Combat Medic on Emerald
|
||||||
|
event := types.PopEvent{
|
||||||
|
WorldID: 17,
|
||||||
|
ZoneID: 4,
|
||||||
|
TeamID: types.TR,
|
||||||
|
LoadoutID: 4,
|
||||||
|
ClassName: translators.CombatMedic,
|
||||||
|
CharacterID: "aaaa",
|
||||||
|
}
|
||||||
|
|
||||||
|
eventPlayer := event.ToPlayer()
|
||||||
|
|
||||||
|
ps.On("Insert", ctx, eventPlayer).Return(nil).Once()
|
||||||
|
|
||||||
|
i.TrackPop(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrackPopFixup(t *testing.T) {
|
||||||
|
ctx, i, ps := mkIngest(t)
|
||||||
|
|
||||||
|
event := types.PopEvent{
|
||||||
|
WorldID: 17,
|
||||||
|
ZoneID: 4,
|
||||||
|
TeamID: 0,
|
||||||
|
ClassName: "unknown",
|
||||||
|
CharacterID: "bbbb",
|
||||||
|
}
|
||||||
|
pastEventPlayer := event.ToPlayer()
|
||||||
|
pastEventPlayer.ClassName = "light_assault"
|
||||||
|
pastEventPlayer.FactionID = types.VS
|
||||||
|
|
||||||
|
ps.On("GetOne", ctx, event.CharacterID).Return(pastEventPlayer, nil).Once()
|
||||||
|
ps.On("Insert", ctx, pastEventPlayer).Return(nil).Once()
|
||||||
|
|
||||||
|
i.TrackPop(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrackPopFixupFailed(t *testing.T) {
|
||||||
|
ctx, i, ps := mkIngest(t)
|
||||||
|
|
||||||
|
event := types.PopEvent{
|
||||||
|
WorldID: 17,
|
||||||
|
ZoneID: 4,
|
||||||
|
TeamID: 0,
|
||||||
|
ClassName: "unknown",
|
||||||
|
CharacterID: "bbbb",
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.On("GetOne", ctx, event.CharacterID).Return(nil, errors.New("ingest fixup failed")).Once()
|
||||||
|
|
||||||
|
i.TrackPop(ctx, event)
|
||||||
|
}
|
70
cmd/ws/ws.go
70
cmd/ws/ws.go
|
@ -2,14 +2,15 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/genudine/saerro-go/types"
|
"github.com/genudine/saerro-go/cmd/ws/eventhandler"
|
||||||
"nhooyr.io/websocket"
|
"github.com/genudine/saerro-go/cmd/ws/wsmanager"
|
||||||
"nhooyr.io/websocket/wsjson"
|
"github.com/genudine/saerro-go/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -18,54 +19,41 @@ func main() {
|
||||||
log.Fatalln("WS_ADDR is not set.")
|
log.Fatalln("WS_ADDR is not set.")
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := sql.Open("sqlite", ":memory:")
|
db, err := util.GetDBConnection(os.Getenv("DB_ADDR"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("database connection failed", err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
eventHandler := eventhandler.NewEventHandler(db)
|
||||||
|
wsm := wsmanager.NewWebsocketManager(eventHandler)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
wsConn, _, err := websocket.Dial(ctx, wsAddr, nil)
|
err = wsm.Connect(ctx, wsAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Connection to ESS failed.", err)
|
log.Fatalln(err)
|
||||||
}
|
|
||||||
defer wsConn.Close(websocket.StatusInternalError, "internal error. bye")
|
|
||||||
|
|
||||||
err = wsjson.Write(ctx, wsConn, map[string]interface{}{
|
|
||||||
"action": "subscribe",
|
|
||||||
"worlds": "all",
|
|
||||||
"eventNames": getEventNames(),
|
|
||||||
"characters": []string{"all"},
|
|
||||||
"service": "event",
|
|
||||||
|
|
||||||
"logicalAndCharactersWithWorlds": true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("subscription write failed", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("subscribe done")
|
go wsm.Start()
|
||||||
|
|
||||||
eventHandler := EventHandler{
|
go func() {
|
||||||
Ingest: &Ingest{
|
time.Sleep(time.Second * 1)
|
||||||
DB: db,
|
err = wsm.Subscribe(ctx)
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
|
||||||
|
|
||||||
var event types.ESSData
|
|
||||||
err := wsjson.Read(ctx, wsConn, &event)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("wsjson read failed", err)
|
wsm.FailClose()
|
||||||
cancel()
|
log.Fatalln("subscribe failed", err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
log.Println("sent subscribe")
|
||||||
|
}()
|
||||||
|
|
||||||
go eventHandler.HandleEvent(ctx, event.Payload)
|
exitSignal := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-exitSignal:
|
||||||
|
log.Println("got interrupt, exiting...")
|
||||||
|
case <-wsm.Closed:
|
||||||
|
log.Println("websocket closed, bailing...")
|
||||||
}
|
}
|
||||||
|
|
||||||
wsConn.Close(websocket.StatusNormalClosure, "")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package wsmanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package wsmanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
124
cmd/ws/wsmanager/wsmanager.go
Normal file
124
cmd/ws/wsmanager/wsmanager.go
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
package wsmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
"github.com/genudine/saerro-go/cmd/ws/eventhandler"
|
||||||
|
"github.com/genudine/saerro-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebsocketManager struct {
|
||||||
|
Conn *websocket.Conn
|
||||||
|
EventHandler eventhandler.EventHandler
|
||||||
|
Closed chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebsocketManager(eh eventhandler.EventHandler) WebsocketManager {
|
||||||
|
return WebsocketManager{
|
||||||
|
EventHandler: eh,
|
||||||
|
Closed: make(chan bool, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsm *WebsocketManager) Connect(ctx context.Context, addr string) (err error) {
|
||||||
|
wsm.Conn, _, err = websocket.Dial(ctx, addr, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wsm: connect failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("wsm: connected to", addr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type ESSSubscription struct {
|
||||||
|
Action string `json:"action,omitempty"`
|
||||||
|
Worlds []string `json:"worlds,omitempty"`
|
||||||
|
EventNames []string `json:"eventNames,omitempty"`
|
||||||
|
Characters []string `json:"characters,omitempty"`
|
||||||
|
Service string `json:"service,omitempty"`
|
||||||
|
LogicalAndCharactersWithWorlds bool `json:"logicalAndCharactersWithWorlds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsm *WebsocketManager) Subscribe(ctx context.Context) error {
|
||||||
|
sub := ESSSubscription{
|
||||||
|
Action: "subscribe",
|
||||||
|
Service: "event",
|
||||||
|
Worlds: []string{"all"},
|
||||||
|
EventNames: getEventNames(),
|
||||||
|
Characters: []string{"all"},
|
||||||
|
LogicalAndCharactersWithWorlds: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := json.NewEncoder(&buf).Encode(sub)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wsm: subscribe: json encode failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("wsm: subscribe message: %s", buf.String())
|
||||||
|
|
||||||
|
err = wsm.Conn.Write(ctx, websocket.MessageText, buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wsm: subscribe: ws write failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsm *WebsocketManager) Start() {
|
||||||
|
go wsm.startWatchdog()
|
||||||
|
|
||||||
|
for {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var event types.ESSData
|
||||||
|
|
||||||
|
_, data, err := wsm.Conn.Read(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("wsm: read failed:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Printf("raw event: %s", string(data))
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, &event)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("wsm: json unmarshal failed:", err)
|
||||||
|
log.Println("wsm: json unmarshal failed (payload)", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
go wsm.EventHandler.HandleEvent(ctx, event.Payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsm *WebsocketManager) startWatchdog() {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Second * 30)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
err := wsm.Conn.Ping(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("wsm: watchdog failed")
|
||||||
|
wsm.Closed <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsm *WebsocketManager) Close() {
|
||||||
|
wsm.Conn.Close(websocket.StatusNormalClosure, "")
|
||||||
|
wsm.Closed <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wsm *WebsocketManager) FailClose() {
|
||||||
|
wsm.Conn.Close(websocket.StatusAbnormalClosure, "")
|
||||||
|
wsm.Closed <- true
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
services:
|
services:
|
||||||
tsdb:
|
tsdb:
|
||||||
image: docker.io/timescale/timescaledb:latest-pg14
|
image: docker.io/timescale/timescaledb:latest-pg14
|
||||||
|
@ -6,5 +5,6 @@ services:
|
||||||
POSTGRES_PASSWORD: saerro321
|
POSTGRES_PASSWORD: saerro321
|
||||||
POSTGRES_USER: saerrouser
|
POSTGRES_USER: saerrouser
|
||||||
POSTGRES_DB: data
|
POSTGRES_DB: data
|
||||||
|
network_mode: host
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
25
go.mod
25
go.mod
|
@ -3,23 +3,38 @@ module github.com/genudine/saerro-go
|
||||||
go 1.22.3
|
go 1.22.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/glebarez/go-sqlite v1.22.0
|
github.com/avast/retry-go v3.0.0+incompatible
|
||||||
|
github.com/coder/websocket v1.8.12
|
||||||
|
github.com/jackc/pgx/v5 v5.7.1
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
nhooyr.io/websocket v1.8.11
|
modernc.org/sqlite v1.33.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
golang.org/x/sys v0.19.0 // indirect
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
golang.org/x/crypto v0.28.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||||
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
|
golang.org/x/text v0.19.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.52.1 // indirect
|
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect
|
||||||
|
modernc.org/libc v1.61.0 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.8.0 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
modernc.org/sqlite v1.30.1 // indirect
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
|
modernc.org/token v1.1.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
77
go.sum
77
go.sum
|
@ -1,13 +1,31 @@
|
||||||
|
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
|
||||||
|
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||||
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
|
||||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
@ -16,29 +34,48 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||||
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
|
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||||
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk=
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
modernc.org/ccgo/v4 v4.17.10 h1:6wrtRozgrhCxieCeJh85QsxkX/2FFrT9hdaWPlbn4Zo=
|
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
|
||||||
modernc.org/ccgo/v4 v4.17.10/go.mod h1:0NBHgsqTTpm9cA5z2ccErvGZmtntSM9qD2kFAs6pjXM=
|
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
|
||||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
||||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M=
|
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 h1:IYXPPTTjjoSHvUClZIYexDiO7g+4x+XveKT4gCIAwiY=
|
||||||
modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ=
|
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
|
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
|
||||||
|
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
|
@ -47,11 +84,9 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk=
|
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
|
||||||
modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
|
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
|
||||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0=
|
|
||||||
nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
|
||||||
|
|
26
shell.nix
26
shell.nix
|
@ -1,4 +1,28 @@
|
||||||
{ pkgs ? import <nixpkgs> {} }: pkgs.mkShell {
|
{ pkgs ? import <nixpkgs> {} }: let
|
||||||
|
podmanSetupScript = let
|
||||||
|
registriesConf = pkgs.writeText "registries.conf" ''
|
||||||
|
[registries.search]
|
||||||
|
registries = ['docker.io']
|
||||||
|
[registries.block]
|
||||||
|
registries = []
|
||||||
|
'';
|
||||||
|
in pkgs.writeScript "podman-setup" ''
|
||||||
|
#!${pkgs.runtimeShell}
|
||||||
|
# Dont overwrite customised configuration
|
||||||
|
if ! test -f ~/.config/containers/policy.json; then
|
||||||
|
install -Dm555 ${pkgs.skopeo.src}/default-policy.json ~/.config/containers/policy.json
|
||||||
|
fi
|
||||||
|
if ! test -f ~/.config/containers/registries.conf; then
|
||||||
|
install -Dm555 ${registriesConf} ~/.config/containers/registries.conf
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Provides a fake "docker" binary mapping to podman
|
||||||
|
dockerCompat = pkgs.runCommandNoCC "docker-podman-compat" {} ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
ln -s ${pkgs.podman}/bin/podman $out/bin/docker
|
||||||
|
'';
|
||||||
|
in pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
go
|
go
|
||||||
just
|
just
|
||||||
|
|
23
store/env_test.go
Normal file
23
store/env_test.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package store_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/util/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mkEnv(t *testing.T) (context.Context, *sql.DB) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
db := testutil.GetTestDB(t)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cancel()
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx, db
|
||||||
|
}
|
123
store/player.go
123
store/player.go
|
@ -5,10 +5,26 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/types"
|
||||||
|
"github.com/genudine/saerro-go/util"
|
||||||
|
|
||||||
|
"github.com/avast/retry-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type IPlayerStore interface {
|
||||||
|
IsMigrated(context.Context) bool
|
||||||
|
RunMigration(context.Context, bool)
|
||||||
|
Insert(context.Context, *types.Player) error
|
||||||
|
GetOne(context.Context, string) (*types.Player, error)
|
||||||
|
Prune(context.Context) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
type PlayerStore struct {
|
type PlayerStore struct {
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
|
|
||||||
|
// Test introspection for if migrations ran during this PlayerStore init
|
||||||
|
RanMigration bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlayerStore(db *sql.DB) *PlayerStore {
|
func NewPlayerStore(db *sql.DB) *PlayerStore {
|
||||||
|
@ -24,10 +40,19 @@ func NewPlayerStore(db *sql.DB) *PlayerStore {
|
||||||
return ps
|
return ps
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *PlayerStore) RunMigration(ctx context.Context, force bool) {
|
func (ps *PlayerStore) IsMigrated(ctx context.Context) bool {
|
||||||
if !force {
|
_, err := ps.DB.QueryContext(ctx, `SELECT count(1) FROM players LIMIT 1;`)
|
||||||
// check if migrated first...
|
if err != nil {
|
||||||
|
log.Printf("IsMigrated check failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PlayerStore) RunMigration(ctx context.Context, force bool) {
|
||||||
|
if !force && ps.IsMigrated(ctx) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("(Re)-creating players table")
|
log.Println("(Re)-creating players table")
|
||||||
|
@ -36,13 +61,101 @@ func (ps *PlayerStore) RunMigration(ctx context.Context, force bool) {
|
||||||
|
|
||||||
CREATE TABLE players (
|
CREATE TABLE players (
|
||||||
character_id TEXT NOT NULL PRIMARY KEY,
|
character_id TEXT NOT NULL PRIMARY KEY,
|
||||||
last_updated TIMESTAMPTZ NOT NULL,
|
last_updated TIMESTAMP NOT NULL,
|
||||||
world_id INT NOT NULL,
|
world_id INT NOT NULL,
|
||||||
faction_id INT NOT NULL,
|
faction_id INT NOT NULL,
|
||||||
zone_id INT NOT NULL,
|
zone_id INT NOT NULL,
|
||||||
class_name TEXT NOT NULL
|
class_name TEXT NOT NULL
|
||||||
);
|
);
|
||||||
`)
|
|
||||||
|
|
||||||
|
-- TODO: Add indexes?
|
||||||
|
`)
|
||||||
log.Println("Done, players table is initialized.")
|
log.Println("Done, players table is initialized.")
|
||||||
|
ps.RanMigration = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a player into the store.
|
||||||
|
// For testing, when LastUpdated is not "zero", the provided timestamp will be carried into the store.
|
||||||
|
func (ps *PlayerStore) Insert(ctx context.Context, player *types.Player) error {
|
||||||
|
if player.LastUpdated.IsZero() {
|
||||||
|
player.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := retry.Do(func() error {
|
||||||
|
_, err := ps.DB.ExecContext(ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO players (
|
||||||
|
last_updated, character_id, world_id, faction_id, zone_id, class_name
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
ON CONFLICT (character_id) DO UPDATE SET
|
||||||
|
last_updated = EXCLUDED.last_updated,
|
||||||
|
world_id = EXCLUDED.world_id,
|
||||||
|
faction_id = EXCLUDED.faction_id,
|
||||||
|
zone_id = EXCLUDED.zone_id,
|
||||||
|
class_name = EXCLUDED.class_name;
|
||||||
|
`,
|
||||||
|
util.TimeToString(player.LastUpdated),
|
||||||
|
player.CharacterID,
|
||||||
|
player.WorldID,
|
||||||
|
player.FactionID,
|
||||||
|
player.ZoneID,
|
||||||
|
player.ClassName,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}, retry.Attempts(2))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOne player from the store.
|
||||||
|
func (ps *PlayerStore) GetOne(ctx context.Context, id string) (*types.Player, error) {
|
||||||
|
row := ps.DB.QueryRowContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
last_updated,
|
||||||
|
character_id,
|
||||||
|
world_id,
|
||||||
|
faction_id,
|
||||||
|
zone_id,
|
||||||
|
class_name
|
||||||
|
FROM players WHERE character_id = $1
|
||||||
|
`, id)
|
||||||
|
|
||||||
|
player := &types.Player{}
|
||||||
|
var timestamp string
|
||||||
|
|
||||||
|
err := row.Scan(
|
||||||
|
×tamp,
|
||||||
|
&player.CharacterID,
|
||||||
|
&player.WorldID,
|
||||||
|
&player.FactionID,
|
||||||
|
&player.ZoneID,
|
||||||
|
&player.ClassName,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
player.LastUpdated, err = time.Parse(time.RFC3339, timestamp)
|
||||||
|
|
||||||
|
return player, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *PlayerStore) Prune(ctx context.Context) (int64, error) {
|
||||||
|
log.Println("pruning PlayerStore")
|
||||||
|
|
||||||
|
// Avoid using sql idioms here for portability
|
||||||
|
// SQLite and PgSQL do now() differently, we don't need to at all.
|
||||||
|
res, err := ps.DB.ExecContext(ctx,
|
||||||
|
`
|
||||||
|
DELETE FROM players WHERE last_updated < $1;
|
||||||
|
`,
|
||||||
|
util.TimeToString(time.Now().Add(-time.Minute*15)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected()
|
||||||
}
|
}
|
||||||
|
|
113
store/player_test.go
Normal file
113
store/player_test.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package store_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/store"
|
||||||
|
"github.com/genudine/saerro-go/types"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlayerMigrationClean(t *testing.T) {
|
||||||
|
ctx, db := mkEnv(t)
|
||||||
|
|
||||||
|
ps := store.NewPlayerStore(db)
|
||||||
|
|
||||||
|
isMigrated := ps.IsMigrated(ctx)
|
||||||
|
assert.True(t, isMigrated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlayerMultipleStartups(t *testing.T) {
|
||||||
|
ctx, db := mkEnv(t)
|
||||||
|
|
||||||
|
ps1 := store.NewPlayerStore(db)
|
||||||
|
ps2 := store.NewPlayerStore(db)
|
||||||
|
|
||||||
|
isMigrated := ps2.IsMigrated(ctx)
|
||||||
|
assert.True(t, isMigrated)
|
||||||
|
|
||||||
|
assert.True(t, ps1.RanMigration)
|
||||||
|
assert.False(t, ps2.RanMigration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlayerMigrationRerun(t *testing.T) {
|
||||||
|
ctx, db := mkEnv(t)
|
||||||
|
|
||||||
|
ps := store.NewPlayerStore(db)
|
||||||
|
ps.RunMigration(ctx, true)
|
||||||
|
|
||||||
|
isMigrated := ps.IsMigrated(ctx)
|
||||||
|
assert.True(t, isMigrated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlayerInsertGetOne(t *testing.T) {
|
||||||
|
ctx, db := mkEnv(t)
|
||||||
|
ps := store.NewPlayerStore(db)
|
||||||
|
|
||||||
|
player := &types.Player{
|
||||||
|
CharacterID: "41666",
|
||||||
|
WorldID: 17,
|
||||||
|
FactionID: types.TR,
|
||||||
|
ZoneID: 4,
|
||||||
|
ClassName: "light_assault",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ps.Insert(ctx, player)
|
||||||
|
assert.NoError(t, err, "Insert failed")
|
||||||
|
|
||||||
|
p1, err := ps.GetOne(ctx, "41666")
|
||||||
|
assert.NoError(t, err, "GetOne failed")
|
||||||
|
assert.Equal(t, "light_assault", p1.ClassName)
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
|
||||||
|
player.ClassName = "combat_medic"
|
||||||
|
player.LastUpdated = time.Time{}
|
||||||
|
err = ps.Insert(ctx, player)
|
||||||
|
assert.NoError(t, err, "Insert failed")
|
||||||
|
|
||||||
|
p2, err := ps.GetOne(ctx, "41666")
|
||||||
|
assert.NoError(t, err, "GetOne failed")
|
||||||
|
assert.Equal(t, "combat_medic", p2.ClassName)
|
||||||
|
assert.NotEqual(t, p1.LastUpdated, p2.LastUpdated, "time did not update as expected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlayerPrune(t *testing.T) {
|
||||||
|
ctx, db := mkEnv(t)
|
||||||
|
ps := store.NewPlayerStore(db)
|
||||||
|
|
||||||
|
prunedPlayer := &types.Player{
|
||||||
|
CharacterID: "20155",
|
||||||
|
WorldID: 17,
|
||||||
|
FactionID: types.NC,
|
||||||
|
ZoneID: 4,
|
||||||
|
ClassName: "light_assault",
|
||||||
|
LastUpdated: time.Now().Add(-time.Minute * 20),
|
||||||
|
}
|
||||||
|
survivingPlayer := &types.Player{
|
||||||
|
CharacterID: "41666",
|
||||||
|
WorldID: 17,
|
||||||
|
FactionID: types.TR,
|
||||||
|
ZoneID: 4,
|
||||||
|
ClassName: "light_assault",
|
||||||
|
LastUpdated: time.Now().Add(-time.Minute * 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ps.Insert(ctx, prunedPlayer)
|
||||||
|
assert.NoError(t, err, "Insert prunedPlayer failed")
|
||||||
|
|
||||||
|
err = ps.Insert(ctx, survivingPlayer)
|
||||||
|
assert.NoError(t, err, "Insert survivingPlayer failed")
|
||||||
|
|
||||||
|
removed, err := ps.Prune(ctx)
|
||||||
|
assert.NoError(t, err, "Prune failed")
|
||||||
|
assert.Equal(t, int64(1), removed, "Prune count incorrect")
|
||||||
|
|
||||||
|
_, err = ps.GetOne(ctx, prunedPlayer.CharacterID)
|
||||||
|
assert.Error(t, err, "GetOne prunedPlayer failed, as expected.")
|
||||||
|
|
||||||
|
_, err = ps.GetOne(ctx, survivingPlayer.CharacterID)
|
||||||
|
assert.NoError(t, err, "GetOne survivingPlayer failed")
|
||||||
|
}
|
45
store/storemock/playerstore.go
Normal file
45
store/storemock/playerstore.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package storemock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/types"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockPlayerStore struct {
|
||||||
|
mock.Mock
|
||||||
|
|
||||||
|
DB *sql.DB
|
||||||
|
RanMigration bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockPlayerStore) IsMigrated(ctx context.Context) bool {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return args.Bool(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockPlayerStore) RunMigration(ctx context.Context, force bool) {
|
||||||
|
m.Called(ctx, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockPlayerStore) Insert(ctx context.Context, player *types.Player) error {
|
||||||
|
args := m.Called(ctx, player)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockPlayerStore) GetOne(ctx context.Context, id string) (*types.Player, error) {
|
||||||
|
args := m.Called(ctx, id)
|
||||||
|
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args.Get(0).(*types.Player), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockPlayerStore) Prune(ctx context.Context) (int64, error) {
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return int64(args.Int(0)), args.Error(1)
|
||||||
|
}
|
149
store/vehicle.go
Normal file
149
store/vehicle.go
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/types"
|
||||||
|
|
||||||
|
"github.com/avast/retry-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VehicleStore struct {
|
||||||
|
DB *sql.DB
|
||||||
|
|
||||||
|
// Test introspection for if migrations ran during this PlayerStore init
|
||||||
|
RanMigration bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVehicleStore(db *sql.DB) *VehicleStore {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ps := &VehicleStore{
|
||||||
|
DB: db,
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.RunMigration(ctx, false)
|
||||||
|
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *VehicleStore) IsMigrated(ctx context.Context) bool {
|
||||||
|
_, err := ps.DB.QueryContext(ctx, `SELECT count(1) FROM vehicles LIMIT 1;`)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("IsMigrated check failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *VehicleStore) RunMigration(ctx context.Context, force bool) {
|
||||||
|
if !force && ps.IsMigrated(ctx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("(Re)-creating vehicles table")
|
||||||
|
ps.DB.ExecContext(ctx, `
|
||||||
|
DROP TABLE IF EXISTS vehicles;
|
||||||
|
|
||||||
|
CREATE TABLE vehicles (
|
||||||
|
character_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
last_updated TIMESTAMP NOT NULL,
|
||||||
|
world_id INT NOT NULL,
|
||||||
|
faction_id INT NOT NULL,
|
||||||
|
zone_id INT NOT NULL,
|
||||||
|
vehicle_name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- TODO: Add indexes?
|
||||||
|
`)
|
||||||
|
log.Println("Done, vehicles table is initialized.")
|
||||||
|
ps.RanMigration = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a player into the store.
|
||||||
|
// For testing, when LastUpdated is not "zero", the provided timestamp will be carried into the store.
|
||||||
|
func (ps *VehicleStore) Insert(ctx context.Context, vehicle *types.Vehicle) error {
|
||||||
|
if vehicle.LastUpdated.IsZero() {
|
||||||
|
vehicle.LastUpdated = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := retry.Do(func() error {
|
||||||
|
_, err := ps.DB.ExecContext(ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO vehicles (
|
||||||
|
last_updated, character_id, world_id, faction_id, zone_id, vehicle_name
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
ON CONFLICT (character_id) DO UPDATE SET
|
||||||
|
last_updated = EXCLUDED.last_updated,
|
||||||
|
world_id = EXCLUDED.world_id,
|
||||||
|
faction_id = EXCLUDED.faction_id,
|
||||||
|
zone_id = EXCLUDED.zone_id,
|
||||||
|
vehicle_name = EXCLUDED.vehicle_name
|
||||||
|
`,
|
||||||
|
vehicle.LastUpdated,
|
||||||
|
vehicle.CharacterID,
|
||||||
|
vehicle.WorldID,
|
||||||
|
vehicle.FactionID,
|
||||||
|
vehicle.ZoneID,
|
||||||
|
vehicle.VehicleName,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}, retry.Attempts(2))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOne player from the store.
|
||||||
|
func (ps *VehicleStore) GetOne(ctx context.Context, id string) (*types.Vehicle, error) {
|
||||||
|
row := ps.DB.QueryRowContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
last_updated,
|
||||||
|
character_id,
|
||||||
|
world_id,
|
||||||
|
faction_id,
|
||||||
|
zone_id,
|
||||||
|
vehicle_name
|
||||||
|
FROM vehicles WHERE character_id = $1
|
||||||
|
`, id)
|
||||||
|
|
||||||
|
vehicle := &types.Vehicle{}
|
||||||
|
var timestamp string
|
||||||
|
|
||||||
|
err := row.Scan(
|
||||||
|
×tamp,
|
||||||
|
&vehicle.CharacterID,
|
||||||
|
&vehicle.WorldID,
|
||||||
|
&vehicle.FactionID,
|
||||||
|
&vehicle.ZoneID,
|
||||||
|
&vehicle.VehicleName,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vehicle.LastUpdated, err = time.Parse(time.RFC3339, timestamp)
|
||||||
|
|
||||||
|
return vehicle, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *VehicleStore) Prune(ctx context.Context) (int64, error) {
|
||||||
|
log.Println("pruning VehicleStore")
|
||||||
|
|
||||||
|
// Avoid using sql idioms here for portability
|
||||||
|
// SQLite and PgSQL do now() differently, we don't need to at all.
|
||||||
|
res, err := ps.DB.ExecContext(ctx, `
|
||||||
|
DELETE FROM vehicles WHERE last_updated < $1;
|
||||||
|
`, time.Now().Add(-time.Minute*15))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected()
|
||||||
|
}
|
114
store/vehicle_test.go
Normal file
114
store/vehicle_test.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package store_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/store"
|
||||||
|
"github.com/genudine/saerro-go/types"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVehicleMigrationClean(t *testing.T) {
|
||||||
|
ctx, db := mkEnv(t)
|
||||||
|
|
||||||
|
ps := store.NewVehicleStore(db)
|
||||||
|
|
||||||
|
isMigrated := ps.IsMigrated(ctx)
|
||||||
|
assert.True(t, isMigrated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVehicleMultipleStartups(t *testing.T) {
|
||||||
|
ctx, db := mkEnv(t)
|
||||||
|
|
||||||
|
ps1 := store.NewVehicleStore(db)
|
||||||
|
ps2 := store.NewVehicleStore(db)
|
||||||
|
|
||||||
|
isMigrated := ps2.IsMigrated(ctx)
|
||||||
|
assert.True(t, isMigrated)
|
||||||
|
|
||||||
|
assert.True(t, ps1.RanMigration)
|
||||||
|
assert.False(t, ps2.RanMigration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVehicleMigrationRerun(t *testing.T) {
|
||||||
|
ctx, db := mkEnv(t)
|
||||||
|
|
||||||
|
ps := store.NewVehicleStore(db)
|
||||||
|
ps.RunMigration(ctx, true)
|
||||||
|
|
||||||
|
isMigrated := ps.IsMigrated(ctx)
|
||||||
|
assert.True(t, isMigrated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVehicleInsertGetOne(t *testing.T) {
|
||||||
|
ctx, db := mkEnv(t)
|
||||||
|
ps := store.NewVehicleStore(db)
|
||||||
|
|
||||||
|
player := &types.Vehicle{
|
||||||
|
CharacterID: "41666",
|
||||||
|
WorldID: 17,
|
||||||
|
FactionID: types.TR,
|
||||||
|
ZoneID: 4,
|
||||||
|
VehicleName: "harasser",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ps.Insert(ctx, player)
|
||||||
|
assert.NoError(t, err, "Insert failed")
|
||||||
|
|
||||||
|
p1, err := ps.GetOne(ctx, "41666")
|
||||||
|
assert.NoError(t, err, "GetOne failed")
|
||||||
|
assert.Equal(t, "harasser", p1.VehicleName)
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
|
||||||
|
player.VehicleName = "vanguard"
|
||||||
|
player.LastUpdated = time.Time{}
|
||||||
|
err = ps.Insert(ctx, player)
|
||||||
|
assert.NoError(t, err, "Insert failed")
|
||||||
|
|
||||||
|
p2, err := ps.GetOne(ctx, "41666")
|
||||||
|
assert.NoError(t, err, "GetOne failed")
|
||||||
|
assert.Equal(t, "vanguard", p2.VehicleName)
|
||||||
|
assert.NotEqual(t, p1.LastUpdated, p2.LastUpdated, "time did not update as expected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVehiclePrune(t *testing.T) {
|
||||||
|
ctx, db := mkEnv(t)
|
||||||
|
ps := store.NewVehicleStore(db)
|
||||||
|
|
||||||
|
prunedPlayer := &types.Vehicle{
|
||||||
|
CharacterID: "20155",
|
||||||
|
WorldID: 17,
|
||||||
|
FactionID: types.NC,
|
||||||
|
ZoneID: 4,
|
||||||
|
VehicleName: "harasser",
|
||||||
|
|
||||||
|
LastUpdated: time.Now().Add(-time.Minute * 20),
|
||||||
|
}
|
||||||
|
survivingPlayer := &types.Vehicle{
|
||||||
|
CharacterID: "41666",
|
||||||
|
WorldID: 17,
|
||||||
|
FactionID: types.TR,
|
||||||
|
ZoneID: 4,
|
||||||
|
VehicleName: "harasser",
|
||||||
|
LastUpdated: time.Now().Add(-time.Minute * 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ps.Insert(ctx, prunedPlayer)
|
||||||
|
assert.NoError(t, err, "Insert prunedPlayer failed")
|
||||||
|
|
||||||
|
err = ps.Insert(ctx, survivingPlayer)
|
||||||
|
assert.NoError(t, err, "Insert survivingPlayer failed")
|
||||||
|
|
||||||
|
removed, err := ps.Prune(ctx)
|
||||||
|
assert.NoError(t, err, "Prune failed")
|
||||||
|
assert.Equal(t, int64(1), removed, "Prune count incorrect")
|
||||||
|
|
||||||
|
_, err = ps.GetOne(ctx, prunedPlayer.CharacterID)
|
||||||
|
assert.Error(t, err, "GetOne prunedPlayer failed, as expected.")
|
||||||
|
|
||||||
|
_, err = ps.GetOne(ctx, survivingPlayer.CharacterID)
|
||||||
|
assert.NoError(t, err, "GetOne survivingPlayer failed")
|
||||||
|
}
|
|
@ -12,40 +12,40 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
LoadoutMap = map[string]Class{
|
LoadoutMap = map[uint16]Class{
|
||||||
"1": Infiltrator,
|
1: Infiltrator,
|
||||||
"8": Infiltrator,
|
8: Infiltrator,
|
||||||
"15": Infiltrator,
|
15: Infiltrator,
|
||||||
"28": Infiltrator,
|
28: Infiltrator,
|
||||||
|
|
||||||
"3": LightAssault,
|
3: LightAssault,
|
||||||
"10": LightAssault,
|
10: LightAssault,
|
||||||
"17": LightAssault,
|
17: LightAssault,
|
||||||
"29": LightAssault,
|
29: LightAssault,
|
||||||
|
|
||||||
"4": CombatMedic,
|
4: CombatMedic,
|
||||||
"11": CombatMedic,
|
11: CombatMedic,
|
||||||
"18": CombatMedic,
|
18: CombatMedic,
|
||||||
"30": CombatMedic,
|
30: CombatMedic,
|
||||||
|
|
||||||
"5": Engineer,
|
5: Engineer,
|
||||||
"12": Engineer,
|
12: Engineer,
|
||||||
"19": Engineer,
|
19: Engineer,
|
||||||
"31": Engineer,
|
31: Engineer,
|
||||||
|
|
||||||
"6": HeavyAssault,
|
6: HeavyAssault,
|
||||||
"13": HeavyAssault,
|
13: HeavyAssault,
|
||||||
"20": HeavyAssault,
|
20: HeavyAssault,
|
||||||
"32": HeavyAssault,
|
32: HeavyAssault,
|
||||||
|
|
||||||
"7": MAX,
|
7: MAX,
|
||||||
"14": MAX,
|
14: MAX,
|
||||||
"21": MAX,
|
21: MAX,
|
||||||
"45": MAX,
|
45: MAX,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func ClassFromLoadout(loadoutID string) Class {
|
func ClassFromLoadout(loadoutID uint16) Class {
|
||||||
c, ok := LoadoutMap[loadoutID]
|
c, ok := LoadoutMap[loadoutID]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -3,11 +3,11 @@ package translators_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/genudine/saerro-go/pkg/translators"
|
"github.com/genudine/saerro-go/translators"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadouts(t *testing.T) {
|
func TestLoadouts(t *testing.T) {
|
||||||
assert.Equal(t, translators.ClassFromLoadout("1"), translators.Infiltrator)
|
assert.Equal(t, translators.ClassFromLoadout(1), translators.Infiltrator)
|
||||||
assert.Equal(t, translators.ClassFromLoadout("0"), translators.Class("unknown"))
|
assert.Equal(t, translators.ClassFromLoadout(0), translators.Class("unknown"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package translators_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/genudine/saerro-go/pkg/translators"
|
"github.com/genudine/saerro-go/translators"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -8,3 +8,18 @@ const (
|
||||||
TR
|
TR
|
||||||
NSO
|
NSO
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// func (f Faction) UnmarshalJSON(b []byte) error {
|
||||||
|
// switch b[0] {
|
||||||
|
// case '1':
|
||||||
|
// f = VS
|
||||||
|
// case '2':
|
||||||
|
// f = NC
|
||||||
|
// case '3':
|
||||||
|
// f = TR
|
||||||
|
// case '4':
|
||||||
|
// f = NSO
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
27
types/internal.go
Normal file
27
types/internal.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
CharacterID string `json:"character_id"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
WorldID uint16 `json:"world_id"`
|
||||||
|
FactionID Faction `json:"faction_id"`
|
||||||
|
ZoneID uint32 `json:"zone_id"`
|
||||||
|
ClassName string `json:"class_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Vehicle struct {
|
||||||
|
CharacterID string `json:"character_id"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
WorldID uint16 `json:"world_id"`
|
||||||
|
FactionID Faction `json:"faction_id"`
|
||||||
|
ZoneID uint32 `json:"zone_id"`
|
||||||
|
VehicleName string `json:"vehicle_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticEvent struct {
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
WorldID uint16 `json:"world_id"`
|
||||||
|
EventName string `json:"event_name"`
|
||||||
|
}
|
|
@ -6,18 +6,23 @@ type ESSData struct {
|
||||||
|
|
||||||
type ESSEvent struct {
|
type ESSEvent struct {
|
||||||
EventName string `json:"event_name"`
|
EventName string `json:"event_name"`
|
||||||
WorldID uint16 `json:"world_id"`
|
WorldID uint16 `json:"world_id,string"`
|
||||||
ZoneID uint32 `json:"zone_id"`
|
ZoneID uint32 `json:"zone_id,string"`
|
||||||
|
|
||||||
CharacterID string `json:"character_id"`
|
CharacterID string `json:"character_id"`
|
||||||
LoadoutID string `json:"loadout_id"`
|
|
||||||
VehicleID string `json:"vehicle_id"`
|
|
||||||
TeamID Faction `json:"team_id"`
|
|
||||||
|
|
||||||
|
// On Death
|
||||||
|
|
||||||
|
VehicleID string `json:"vehicle_id"`
|
||||||
|
TeamID Faction `json:"team_id,string"`
|
||||||
|
CharacterLoadoutID uint16 `json:"character_loadout_id,string"`
|
||||||
AttackerCharacterID string `json:"attacker_character_id"`
|
AttackerCharacterID string `json:"attacker_character_id"`
|
||||||
AttackerLoadoutID string `json:"attacker_loadout_id"`
|
AttackerLoadoutID uint16 `json:"attacker_loadout_id,string"`
|
||||||
AttackerVehicleID string `json:"attacker_vehicle_id"`
|
AttackerVehicleID string `json:"attacker_vehicle_id"`
|
||||||
AttackerTeamID Faction `json:"attacker_team_id"`
|
AttackerTeamID Faction `json:"attacker_team_id,string"`
|
||||||
|
|
||||||
ExperienceID uint32
|
// On GainExperience
|
||||||
|
|
||||||
|
ExperienceID uint32 `json:"experience_id,string"`
|
||||||
|
LoadoutID uint16 `json:"loadout_id,string"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
package main
|
// TODO: tests
|
||||||
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/genudine/saerro-go/translators"
|
"github.com/genudine/saerro-go/translators"
|
||||||
"github.com/genudine/saerro-go/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PopEvent struct {
|
type PopEvent struct {
|
||||||
WorldID uint16
|
WorldID uint16
|
||||||
ZoneID uint32
|
ZoneID uint32
|
||||||
CharacterID string
|
CharacterID string
|
||||||
LoadoutID string
|
LoadoutID uint16
|
||||||
TeamID types.Faction
|
TeamID Faction
|
||||||
VehicleID string
|
VehicleID string
|
||||||
|
|
||||||
VehicleName translators.Vehicle
|
VehicleName translators.Vehicle
|
||||||
ClassName translators.Class
|
ClassName translators.Class
|
||||||
}
|
}
|
||||||
|
|
||||||
func PopEventFromESSEvent(event types.ESSEvent, attacker bool) PopEvent {
|
func PopEventFromESSEvent(event ESSEvent, attacker bool) PopEvent {
|
||||||
pe := PopEvent{
|
pe := PopEvent{
|
||||||
WorldID: event.WorldID,
|
WorldID: event.WorldID,
|
||||||
ZoneID: event.ZoneID,
|
ZoneID: event.ZoneID,
|
||||||
|
@ -25,7 +25,7 @@ func PopEventFromESSEvent(event types.ESSEvent, attacker bool) PopEvent {
|
||||||
|
|
||||||
if !attacker {
|
if !attacker {
|
||||||
pe.CharacterID = event.CharacterID
|
pe.CharacterID = event.CharacterID
|
||||||
pe.LoadoutID = event.LoadoutID
|
pe.LoadoutID = event.CharacterLoadoutID
|
||||||
pe.TeamID = event.TeamID
|
pe.TeamID = event.TeamID
|
||||||
pe.VehicleID = event.VehicleID
|
pe.VehicleID = event.VehicleID
|
||||||
} else {
|
} else {
|
||||||
|
@ -35,8 +35,22 @@ func PopEventFromESSEvent(event types.ESSEvent, attacker bool) PopEvent {
|
||||||
pe.VehicleID = event.AttackerVehicleID
|
pe.VehicleID = event.AttackerVehicleID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pe.LoadoutID == 0 {
|
||||||
|
pe.LoadoutID = event.LoadoutID
|
||||||
|
}
|
||||||
|
|
||||||
pe.ClassName = translators.ClassFromLoadout(pe.LoadoutID)
|
pe.ClassName = translators.ClassFromLoadout(pe.LoadoutID)
|
||||||
pe.VehicleName = translators.VehicleNameFromID(pe.VehicleID)
|
pe.VehicleName = translators.VehicleNameFromID(pe.VehicleID)
|
||||||
|
|
||||||
return pe
|
return pe
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pe PopEvent) ToPlayer() *Player {
|
||||||
|
return &Player{
|
||||||
|
CharacterID: pe.CharacterID,
|
||||||
|
ClassName: string(pe.ClassName),
|
||||||
|
FactionID: pe.TeamID,
|
||||||
|
ZoneID: pe.ZoneID,
|
||||||
|
WorldID: pe.WorldID,
|
||||||
|
}
|
||||||
|
}
|
72
types/pop_event_test.go
Normal file
72
types/pop_event_test.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package types_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/genudine/saerro-go/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
deathEvent = types.ESSEvent{
|
||||||
|
EventName: "Death",
|
||||||
|
WorldID: 17,
|
||||||
|
ZoneID: 4,
|
||||||
|
|
||||||
|
CharacterID: "LyytisDoll",
|
||||||
|
TeamID: types.NC,
|
||||||
|
CharacterLoadoutID: 4,
|
||||||
|
|
||||||
|
AttackerCharacterID: "Lyyti",
|
||||||
|
AttackerTeamID: types.TR,
|
||||||
|
AttackerLoadoutID: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
expEvent = types.ESSEvent{
|
||||||
|
EventName: "GainExperience",
|
||||||
|
WorldID: 17,
|
||||||
|
ZoneID: 4,
|
||||||
|
|
||||||
|
CharacterID: "LyytisDoll",
|
||||||
|
TeamID: types.NC,
|
||||||
|
LoadoutID: 3,
|
||||||
|
|
||||||
|
ExperienceID: 55,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPopEventFromDeath(t *testing.T) {
|
||||||
|
victim := types.PopEventFromESSEvent(deathEvent, false)
|
||||||
|
assert.Equal(t, deathEvent.CharacterID, victim.CharacterID)
|
||||||
|
assert.Equal(t, deathEvent.TeamID, victim.TeamID)
|
||||||
|
assert.Equal(t, deathEvent.CharacterLoadoutID, victim.LoadoutID)
|
||||||
|
assert.Equal(t, "combat_medic", string(victim.ClassName))
|
||||||
|
|
||||||
|
attacker := types.PopEventFromESSEvent(deathEvent, true)
|
||||||
|
assert.Equal(t, deathEvent.AttackerCharacterID, attacker.CharacterID)
|
||||||
|
assert.Equal(t, deathEvent.AttackerTeamID, attacker.TeamID)
|
||||||
|
assert.Equal(t, deathEvent.AttackerLoadoutID, attacker.LoadoutID)
|
||||||
|
assert.Equal(t, "light_assault", string(attacker.ClassName))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPopEventFromExperienceGain(t *testing.T) {
|
||||||
|
pe := types.PopEventFromESSEvent(expEvent, false)
|
||||||
|
assert.Equal(t, expEvent.CharacterID, pe.CharacterID)
|
||||||
|
assert.Equal(t, expEvent.TeamID, pe.TeamID)
|
||||||
|
assert.Equal(t, expEvent.LoadoutID, pe.LoadoutID)
|
||||||
|
assert.Equal(t, "light_assault", string(pe.ClassName))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPopEventFromVehicleDestroy(t *testing.T) {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPopEventToPlayer(t *testing.T) {
|
||||||
|
pe := types.PopEventFromESSEvent(deathEvent, false)
|
||||||
|
player := pe.ToPlayer()
|
||||||
|
assert.Equal(t, pe.CharacterID, player.CharacterID)
|
||||||
|
assert.Equal(t, pe.TeamID, player.FactionID)
|
||||||
|
assert.Equal(t, pe.ZoneID, player.ZoneID)
|
||||||
|
assert.Equal(t, string(pe.ClassName), player.ClassName)
|
||||||
|
assert.Equal(t, pe.WorldID, player.WorldID)
|
||||||
|
}
|
22
util/db_connector.go
Normal file
22
util/db_connector.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDBConnection(addr string) (*sql.DB, error) {
|
||||||
|
db, err := sql.Open("pgx", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db failed to open, %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db failed to ping, %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
20
util/testutil/db.go
Normal file
20
util/testutil/db.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTestDB standardizes what type of DB tests use.
|
||||||
|
func GetTestDB(t *testing.T) *sql.DB {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite", t.TempDir()+"/test.db")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("test shim: sqlite open failed, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
50
util/testutil/ws.go
Normal file
50
util/testutil/ws.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockESS struct {
|
||||||
|
Server *httptest.Server
|
||||||
|
LastMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MockESS) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
c, err := websocket.Accept(rw, req, nil)
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(400)
|
||||||
|
rw.Write([]byte("websocket connection failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.CloseNow()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(req.Context(), time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, body, err := c.Read(ctx)
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(500)
|
||||||
|
rw.Write([]byte("websocket read failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.LastMessage = string(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMockESS(t *testing.T) MockESS {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
m := MockESS{}
|
||||||
|
|
||||||
|
s := httptest.NewServer(m)
|
||||||
|
m.Server = s
|
||||||
|
|
||||||
|
t.Cleanup(s.Close)
|
||||||
|
return m
|
||||||
|
}
|
8
util/timestrings.go
Normal file
8
util/timestrings.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Makes times compatible with old Saerro API
|
||||||
|
func TimeToString(t time.Time) string {
|
||||||
|
return t.UTC().Format(time.RFC3339)
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue