diff --git a/cmd/cmd.go b/cmd/cmd.go index daaa2431..34150c56 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -10,10 +10,8 @@ import ( "os/exec" "runtime" "strings" - "time" "github.com/hashicorp/go-version" - wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime" "github.com/williamsjokvist/cfn-tracker/pkg/browser" "github.com/williamsjokvist/cfn-tracker/pkg/config" @@ -25,16 +23,16 @@ import ( "github.com/williamsjokvist/cfn-tracker/pkg/storage/nosql" "github.com/williamsjokvist/cfn-tracker/pkg/storage/sql" "github.com/williamsjokvist/cfn-tracker/pkg/storage/txt" - "github.com/williamsjokvist/cfn-tracker/pkg/tracker" - "github.com/williamsjokvist/cfn-tracker/pkg/tracker/sf6" - "github.com/williamsjokvist/cfn-tracker/pkg/tracker/sfv" - "github.com/williamsjokvist/cfn-tracker/pkg/tracker/t8" ) +type CmdHandler interface { + SetContext(ctx context.Context) +} + // The CommandHandler is the interface between the GUI and the core type CommandHandler struct { - ctx context.Context - tracker tracker.GameTracker + ctx context.Context + browser *browser.Browser sqlDb *sql.Storage @@ -44,6 +42,8 @@ type CommandHandler struct { cfg *config.Config } +var _ CmdHandler = (*CommandHandler)(nil) + func NewCommandHandler(browser *browser.Browser, sqlDb *sql.Storage, nosqlDb *nosql.Storage, txtDb *txt.Storage, cfg *config.Config) *CommandHandler { return &CommandHandler{ sqlDb: sqlDb, @@ -59,26 +59,6 @@ func (ch *CommandHandler) SetContext(ctx context.Context) { ch.ctx = ctx } -func (ch *CommandHandler) GetAppVersion() string { - return ch.cfg.AppVersion -} - -func (ch *CommandHandler) GetTranslation(locale string) (*locales.Localization, error) { - lng, err := i18n.GetTranslation(locale) - if err != nil { - log.Println(err) - if !errorsx.ContainsFormattedError(err) { - err = errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf(`failed to get translation %w`, err)) - } - return nil, err - } - return lng, nil -} - -func (ch *CommandHandler) GetSupportedLanguages() []string { - return i18n.GetSupportedLanguages() -} - func (ch *CommandHandler) CheckForUpdate() (bool, error) { currentVersion, err := version.NewVersion(ch.cfg.AppVersion) if err != nil { @@ -96,21 +76,20 @@ func (ch *CommandHandler) CheckForUpdate() (bool, error) { return hasUpdate, nil } -func (ch *CommandHandler) StopTracking() { - log.Println(`Stopped tracking`) - ch.tracker.Stop() -} - -func (ch *CommandHandler) StartTracking(cfn string, restore bool) error { - log.Printf(`Starting tracking for %s, restoring = %v`, cfn, restore) - err := ch.tracker.Start(ch.ctx, cfn, restore, 30*time.Second) +func (ch *CommandHandler) GetTranslation(locale string) (*locales.Localization, error) { + lng, err := i18n.GetTranslation(locale) if err != nil { log.Println(err) if !errorsx.ContainsFormattedError(err) { - err = errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to start tracking %w`, err)) + err = errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf(`failed to get translation %w`, err)) } + return nil, err } - return err + return lng, nil +} + +func (ch *CommandHandler) GetAppVersion() string { + return ch.cfg.AppVersion } func (ch *CommandHandler) OpenResultsDirectory() { @@ -192,44 +171,8 @@ func (ch *CommandHandler) GetThemes() ([]model.Theme, error) { return combinedThemes, nil } -func (ch *CommandHandler) SelectGame(game string) error { - var username, password string - - switch game { - case tracker.GameTypeT8.String(): - ch.tracker = t8.NewT8Tracker(ch.sqlDb, ch.txtDb) - case tracker.GameTypeSF6.String(): - ch.tracker = sf6.NewSF6Tracker(ch.browser, ch.sqlDb, ch.txtDb) - username = ch.cfg.CapIDEmail - password = ch.cfg.CapIDPassword - case tracker.GameTypeSFV.String(): - ch.tracker = sfv.NewSFVTracker(ch.browser) - username = ch.cfg.SteamUsername - password = ch.cfg.SteamPassword - default: - return errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to select game`)) - } - - authChan := make(chan tracker.AuthStatus) - go ch.tracker.Authenticate(username, password, authChan) - for status := range authChan { - if status.Err != nil { - return errorsx.NewFormattedError(http.StatusUnauthorized, status.Err) - } - wailsRuntime.EventsEmit(ch.ctx, "auth-progress", status.Progress) - - if status.Progress >= 100 { - close(authChan) - break - } - } - return nil -} - -func (ch *CommandHandler) ForcePoll() { - if ch.tracker != nil { - ch.tracker.ForcePoll() - } +func (ch *CommandHandler) GetSupportedLanguages() []string { + return i18n.GetSupportedLanguages() } func (ch *CommandHandler) SaveLocale(locale string) error { @@ -248,10 +191,6 @@ func (ch *CommandHandler) SaveTheme(theme model.ThemeName) error { return ch.nosqlDb.SaveTheme(theme) } -func (ch *CommandHandler) GetTrackingStateUnused() *model.TrackingState { - return nil -} - func (ch *CommandHandler) GetFormattedErrorModelUnused() *errorsx.FormattedError { return nil } diff --git a/cmd/tracking.go b/cmd/tracking.go new file mode 100644 index 00000000..a471f2f3 --- /dev/null +++ b/cmd/tracking.go @@ -0,0 +1,162 @@ +package cmd + +import ( + "context" + "fmt" + "log" + "net/http" + "time" + + wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime" + + "github.com/williamsjokvist/cfn-tracker/pkg/browser" + "github.com/williamsjokvist/cfn-tracker/pkg/config" + "github.com/williamsjokvist/cfn-tracker/pkg/errorsx" + "github.com/williamsjokvist/cfn-tracker/pkg/model" + "github.com/williamsjokvist/cfn-tracker/pkg/storage/nosql" + "github.com/williamsjokvist/cfn-tracker/pkg/storage/sql" + "github.com/williamsjokvist/cfn-tracker/pkg/storage/txt" + "github.com/williamsjokvist/cfn-tracker/pkg/tracker" + "github.com/williamsjokvist/cfn-tracker/pkg/tracker/sf6" + _ "github.com/williamsjokvist/cfn-tracker/pkg/tracker/sfv" + "github.com/williamsjokvist/cfn-tracker/pkg/tracker/t8" +) + +type TrackingHandler struct { + ctx context.Context + gameTracker tracker.GameTracker + + browser *browser.Browser + + cancelPolling context.CancelFunc + forcePollChan chan struct{} + + sqlDb *sql.Storage + nosqlDb *nosql.Storage + txtDb *txt.Storage + + cfg *config.Config +} + +var _ CmdHandler = (*TrackingHandler)(nil) + +func NewTrackingHandler(browser *browser.Browser, sqlDb *sql.Storage, nosqlDb *nosql.Storage, txtDb *txt.Storage, cfg *config.Config) *TrackingHandler { + return &TrackingHandler{ + sqlDb: sqlDb, + nosqlDb: nosqlDb, + txtDb: txtDb, + browser: browser, + cfg: cfg, + } +} + +// The CommandHandler needs the wails runtime context in order to emit events +func (ch *TrackingHandler) SetContext(ctx context.Context) { + ch.ctx = ctx +} + +func (ch *TrackingHandler) StartTracking(userCode string, restore bool) { + log.Printf(`Starting tracking for %s, restoring = %v`, userCode, restore) + ticker := time.NewTicker(30 * time.Second) + pollCtx, cancel := context.WithCancel(ch.ctx) + ch.cancelPolling = cancel + ch.forcePollChan = make(chan struct{}) + var matchChan = make(chan model.Match) + + defer func() { + ticker.Stop() + cancel() + close(ch.forcePollChan) + ch.forcePollChan = nil + wailsRuntime.EventsEmit(ch.ctx, "stopped-tracking") + log.Println("stopped polling") + }() + + session, err := ch.gameTracker.Init(pollCtx, userCode, restore) + if err != nil { + return + } + + go func() { + log.Println("polling") + ch.gameTracker.Poll(pollCtx, cancel, session, matchChan) + for { + select { + case <-ch.forcePollChan: + log.Println("forced poll") + ch.gameTracker.Poll(pollCtx, cancel, session, matchChan) + case <-ticker.C: + log.Println("polling") + ch.gameTracker.Poll(pollCtx, cancel, session, matchChan) + case <-pollCtx.Done(): + close(matchChan) + return + } + } + }() + + for match := range matchChan { + session.LP = match.LP + session.MR = match.MR + session.Matches = append([]*model.Match{&match}, session.Matches...) + if err := ch.sqlDb.UpdateSession(ch.ctx, session); err != nil { + log.Println("failed to update session", err) + return + } + if err := ch.sqlDb.SaveMatch(ch.ctx, match); err != nil { + log.Println("failed to save match", err) + return + } + + wailsRuntime.EventsEmit(ch.ctx, `cfn-data`, match) + if err := ch.txtDb.SaveMatch(match); err != nil { + log.Print("failed to save tracking state:", err) + return + } + } +} + +func (ch *TrackingHandler) StopTracking() { + ch.cancelPolling() +} + +func (ch *TrackingHandler) SelectGame(game model.GameType) error { + var username, password string + + switch game { + case model.GameTypeT8: + ch.gameTracker = t8.NewT8Tracker(ch.sqlDb, ch.txtDb) + case model.GameTypeSF6: + ch.gameTracker = sf6.NewSF6Tracker(ch.browser, ch.sqlDb, ch.txtDb) + username = ch.cfg.CapIDEmail + password = ch.cfg.CapIDPassword + case model.GameTypeSFV: + // gameTracker = sfv.NewSFVTracker(ch.browser) + // username = ch.cfg.SteamUsername + // password = ch.cfg.SteamPassword + fallthrough + default: + return errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to select game`)) + } + + authChan := make(chan tracker.AuthStatus) + go ch.gameTracker.Authenticate(username, password, authChan) + for status := range authChan { + if status.Err != nil { + return errorsx.NewFormattedError(http.StatusUnauthorized, status.Err) + } + wailsRuntime.EventsEmit(ch.ctx, "auth-progress", status.Progress) + + if status.Progress >= 100 { + close(authChan) + break + } + } + return nil +} + +func (ch *TrackingHandler) ForcePoll() { + if ch.forcePollChan != nil { + ch.forcePollChan <- struct{}{} + } +} diff --git a/gui/.config/vite.config.js b/gui/.config/vite.config.js index 4b04f681..1d8dd317 100644 --- a/gui/.config/vite.config.js +++ b/gui/.config/vite.config.js @@ -15,7 +15,7 @@ export default defineConfig({ '@@': path.resolve(dirname, "wailsjs"), "@runtime": path.resolve(dirname, "wailsjs", "runtime", "runtime.js"), "@model": path.resolve(dirname, "wailsjs", "go", "models.ts"), - "@cmd": path.resolve(dirname, "wailsjs", "go", "cmd", "CommandHandler.js"), + "@cmd": path.resolve(dirname, "wailsjs", "go", "cmd"), }, }, css: { diff --git a/gui/package.json.md5 b/gui/package.json.md5 index b53e8c21..90629c38 100644 --- a/gui/package.json.md5 +++ b/gui/package.json.md5 @@ -1 +1 @@ -f5aab2da6a3208cbfc7296b675f1fcbc \ No newline at end of file +42fe0975641ef40aa09a3006fecfb6c5 \ No newline at end of file diff --git a/gui/src/main/app-wrapper.tsx b/gui/src/main/app-wrapper.tsx index 700f4d33..1c356c0c 100644 --- a/gui/src/main/app-wrapper.tsx +++ b/gui/src/main/app-wrapper.tsx @@ -6,7 +6,7 @@ import { Icon } from '@iconify/react' import { cn } from '@/helpers/cn' import { AuthMachineContext } from '@/state/auth-machine' -import { CheckForUpdate } from '@cmd' +import { CheckForUpdate } from '@cmd/CommandHandler' import { BrowserOpenURL } from '@runtime' import { useErrorPopup } from './error-popup' diff --git a/gui/src/main/config.tsx b/gui/src/main/config.tsx index 5c84bb8e..fec855f6 100644 --- a/gui/src/main/config.tsx +++ b/gui/src/main/config.tsx @@ -2,7 +2,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' import type { model } from '@model' -import { GetGuiConfig } from '@cmd' +import { GetGuiConfig } from '@cmd/CommandHandler' const initialConfig: model.GuiConfig = { locale: 'en-GB', diff --git a/gui/src/main/i18n.tsx b/gui/src/main/i18n.tsx index dc99687e..28142800 100644 --- a/gui/src/main/i18n.tsx +++ b/gui/src/main/i18n.tsx @@ -5,7 +5,7 @@ import HttpBackend, { type HttpBackendOptions } from 'i18next-http-backend' import { I18nextProvider, initReactI18next } from 'react-i18next' import type { locales } from '@model' -import { GetTranslation } from '@cmd' +import { GetTranslation } from '@cmd/CommandHandler' export type LocalizationKey = keyof locales.Localization diff --git a/gui/src/main/router.tsx b/gui/src/main/router.tsx index 124d0458..6e008808 100644 --- a/gui/src/main/router.tsx +++ b/gui/src/main/router.tsx @@ -9,7 +9,7 @@ import { TrackingPage } from '@/pages/tracking' import { AppWrapper } from './app-wrapper' import { AppErrorBoundary, PageErrorBoundary } from './app-error' -import { GetUsers, GetSessions, GetMatches } from '@cmd' +import { GetUsers, GetSessions, GetMatches } from '@cmd/CommandHandler' const router = createHashRouter([ { diff --git a/gui/src/pages/output.tsx b/gui/src/pages/output.tsx index 1dbbba61..1d398570 100644 --- a/gui/src/pages/output.tsx +++ b/gui/src/pages/output.tsx @@ -10,17 +10,17 @@ import { Button } from '@/ui/button' import { Checkbox } from '@/ui/checkbox' import { model } from '@model' -import { GetThemes, OpenResultsDirectory } from '@cmd' +import { GetThemes, OpenResultsDirectory } from '@cmd/CommandHandler' import { useErrorPopup } from '@/main/error-popup' -type StatOptions = Omit, 'totalLosses' | 'totalWins'> & { +type StatOptions = Omit, 'replayId' | 'sessionId'> & { theme: string } const defaultOptions: StatOptions = { theme: 'default', - cfn: false, + userName: false, wins: true, losses: true, winRate: true, @@ -32,12 +32,12 @@ const defaultOptions: StatOptions = { opponent: false, opponentCharacter: false, opponentLeague: false, - opponentLP: false, - totalMatches: false, + opponentLp: false, + opponentMr: false, character: false, - result: false, - userCode: false, - timestamp: false, + victory: false, + userId: false, + time: false, date: false } diff --git a/gui/src/pages/settings.tsx b/gui/src/pages/settings.tsx index 59306962..229701c8 100644 --- a/gui/src/pages/settings.tsx +++ b/gui/src/pages/settings.tsx @@ -12,7 +12,7 @@ import { SaveLocale, SaveSidebarMinimized, SaveTheme -} from '@cmd' +} from '@cmd/CommandHandler' import { BrowserOpenURL } from '@runtime' import * as Page from '@/ui/page' diff --git a/gui/src/pages/tracking/tracking-live-updater.tsx b/gui/src/pages/tracking/tracking-live-updater.tsx index 1c1bf0a0..5dee5098 100644 --- a/gui/src/pages/tracking/tracking-live-updater.tsx +++ b/gui/src/pages/tracking/tracking-live-updater.tsx @@ -16,7 +16,6 @@ export function TrackingLiveUpdater() { const trackingActor = TrackingMachineContext.useActorRef() const { - cfn, lp, mr, wins, @@ -29,7 +28,8 @@ export function TrackingLiveUpdater() { opponentCharacter, character, opponentLeague, - result + userName, + victory } = useSelector(trackingActor, ({ context }) => context.trackingState) const [refreshDisabled, setRefreshDisabled] = React.useState(false) @@ -47,7 +47,7 @@ export function TrackingLiveUpdater() { className='h-full px-6 pt-4' >
- +
{lp > 0 && } {mr > 0 && } @@ -78,7 +78,7 @@ export function TrackingLiveUpdater() { {t('lastMatch')}
{' '} vs diff --git a/gui/src/state/auth-machine.ts b/gui/src/state/auth-machine.ts index b4937402..d0273a2f 100644 --- a/gui/src/state/auth-machine.ts +++ b/gui/src/state/auth-machine.ts @@ -1,7 +1,7 @@ import { setup, assign } from 'xstate' import { createActorContext } from '@xstate/react' -import { SelectGame } from '@cmd' +import { SelectGame } from '@cmd/TrackingHandler' import type { errorsx } from '@model' import { EventsOff, EventsOn } from '@runtime' diff --git a/gui/src/state/tracking-machine.ts b/gui/src/state/tracking-machine.ts index 57462208..d3b20f0a 100644 --- a/gui/src/state/tracking-machine.ts +++ b/gui/src/state/tracking-machine.ts @@ -1,7 +1,7 @@ import { assign, setup } from 'xstate' import { createActorContext } from '@xstate/react' -import { ForcePoll, StartTracking, StopTracking } from '@cmd' +import { ForcePoll, StartTracking, StopTracking } from '@cmd/TrackingHandler' import type { errorsx, model } from '@model' import { EventsOff, EventsOn } from '@runtime' @@ -9,7 +9,7 @@ type TrackingMachineContextProps = { user: model.User | null restore: boolean isTracking: boolean - trackingState: model.TrackingState + trackingState: model.Match error: errorsx.FormattedError | null } @@ -49,7 +49,7 @@ export const TRACKING_MACHINE = setup({ error: null, restore: false, isTracking: false, - trackingState: {} + trackingState: {} }, initial: 'cfnForm', states: { diff --git a/gui/tsconfig.json b/gui/tsconfig.json index a30579fa..6e19d2c3 100644 --- a/gui/tsconfig.json +++ b/gui/tsconfig.json @@ -21,7 +21,7 @@ "@/*": ["src/*"], "@runtime": ["wailsjs/runtime/runtime"], "@model": ["wailsjs/go/models"], - "@cmd": ["wailsjs/go/cmd/CommandHandler"] + "@cmd/*": ["wailsjs/go/cmd/*"], }, "types": ["node"] }, diff --git a/gui/wailsjs/go/cmd/CommandHandler.d.ts b/gui/wailsjs/go/cmd/CommandHandler.d.ts index 3479e7b4..b0229007 100755 --- a/gui/wailsjs/go/cmd/CommandHandler.d.ts +++ b/gui/wailsjs/go/cmd/CommandHandler.d.ts @@ -7,8 +7,6 @@ import {context} from '../models'; export function CheckForUpdate():Promise; -export function ForcePoll():Promise; - export function GetAppVersion():Promise; export function GetFormattedErrorModelUnused():Promise; @@ -23,8 +21,6 @@ export function GetSupportedLanguages():Promise>; export function GetThemes():Promise>; -export function GetTrackingStateUnused():Promise; - export function GetTranslation(arg1:string):Promise; export function GetUsers():Promise>; @@ -37,10 +33,4 @@ export function SaveSidebarMinimized(arg1:boolean):Promise; export function SaveTheme(arg1:model.ThemeName):Promise; -export function SelectGame(arg1:string):Promise; - export function SetContext(arg1:context.Context):Promise; - -export function StartTracking(arg1:string,arg2:boolean):Promise; - -export function StopTracking():Promise; diff --git a/gui/wailsjs/go/cmd/TrackingHandler.d.ts b/gui/wailsjs/go/cmd/TrackingHandler.d.ts new file mode 100755 index 00000000..abf8605f --- /dev/null +++ b/gui/wailsjs/go/cmd/TrackingHandler.d.ts @@ -0,0 +1,14 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT +import {model} from '../models'; +import {context} from '../models'; + +export function ForcePoll():Promise; + +export function SelectGame(arg1:model.GameType):Promise; + +export function SetContext(arg1:context.Context):Promise; + +export function StartTracking(arg1:string,arg2:boolean):Promise; + +export function StopTracking():Promise; diff --git a/gui/wailsjs/go/models.ts b/gui/wailsjs/go/models.ts index 38d1d0f5..49f77e13 100755 --- a/gui/wailsjs/go/models.ts +++ b/gui/wailsjs/go/models.ts @@ -304,6 +304,8 @@ export namespace model { matches: Match[]; matchesWon: number; matchesLost: number; + endingLp: number; + endingMr: number; startingLp: number; startingMr: number; lpGain: number; @@ -324,6 +326,8 @@ export namespace model { this.matches = this.convertValues(source["matches"], Match); this.matchesWon = source["matchesWon"]; this.matchesLost = source["matchesLost"]; + this.endingLp = source["endingLp"]; + this.endingMr = source["endingMr"]; this.startingLp = source["startingLp"]; this.startingMr = source["startingMr"]; this.lpGain = source["lpGain"]; @@ -362,58 +366,6 @@ export namespace model { this.css = source["css"]; } } - export class TrackingState { - cfn: string; - userCode: string; - lp: number; - lpGain: number; - mr: number; - mrGain: number; - wins: number; - totalWins: number; - totalLosses: number; - totalMatches: number; - losses: number; - winRate: number; - character: string; - opponent: string; - opponentCharacter: string; - opponentLP: number; - opponentLeague: string; - result: boolean; - timestamp: string; - date: string; - winStreak: number; - - static createFrom(source: any = {}) { - return new TrackingState(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.cfn = source["cfn"]; - this.userCode = source["userCode"]; - this.lp = source["lp"]; - this.lpGain = source["lpGain"]; - this.mr = source["mr"]; - this.mrGain = source["mrGain"]; - this.wins = source["wins"]; - this.totalWins = source["totalWins"]; - this.totalLosses = source["totalLosses"]; - this.totalMatches = source["totalMatches"]; - this.losses = source["losses"]; - this.winRate = source["winRate"]; - this.character = source["character"]; - this.opponent = source["opponent"]; - this.opponentCharacter = source["opponentCharacter"]; - this.opponentLP = source["opponentLP"]; - this.opponentLeague = source["opponentLeague"]; - this.result = source["result"]; - this.timestamp = source["timestamp"]; - this.date = source["date"]; - this.winStreak = source["winStreak"]; - } - } export class User { id: number; displayName: string; diff --git a/main.go b/main.go index 01d3c979..60012147 100644 --- a/main.go +++ b/main.go @@ -149,7 +149,10 @@ func main() { closeWithError(fmt.Errorf(`failed to initalize text store: %w`, err)) return } + cmdHandler := cmd.NewCommandHandler(appBrowser, sqlDb, noSqlDb, txtDb, &cfg) + trackingHandler := cmd.NewTrackingHandler(appBrowser, sqlDb, noSqlDb, txtDb, &cfg) + cmdHandlers := []cmd.CmdHandler{cmdHandler, trackingHandler} var wailsCtx context.Context err = wails.Run(&options.App{ @@ -184,7 +187,9 @@ func main() { }, OnStartup: func(ctx context.Context) { wailsCtx = ctx - cmdHandler.SetContext(ctx) + for _, c := range cmdHandlers { + c.SetContext(ctx) + } go server.Start(ctx, &cfg) }, OnShutdown: func(_ context.Context) { @@ -206,6 +211,7 @@ func main() { }, Bind: []interface{}{ cmdHandler, + trackingHandler, }, }) if err != nil { diff --git a/pkg/model/conv.go b/pkg/model/conv.go deleted file mode 100644 index 6ef59d4e..00000000 --- a/pkg/model/conv.go +++ /dev/null @@ -1,24 +0,0 @@ -package model - -func ConvMatchToTrackingState(m Match) TrackingState { - return TrackingState{ - CFN: m.UserName, - UserCode: m.UserId, - Wins: m.Wins, - Losses: m.Losses, - WinRate: m.WinRate, - WinStreak: m.WinStreak, - MR: m.MR, - LP: m.LP, - LPGain: m.LPGain, - MRGain: m.MRGain, - Character: m.Character, - IsWin: m.Victory, - Opponent: m.Opponent, - OpponentCharacter: m.OpponentCharacter, - OpponentLP: m.OpponentLP, - OpponentLeague: m.OpponentLeague, - Date: m.Date, - TimeStamp: m.Time, - } -} diff --git a/pkg/model/match.go b/pkg/model/match.go index 61c72d0e..bf8c86d9 100644 --- a/pkg/model/match.go +++ b/pkg/model/match.go @@ -23,3 +23,11 @@ type Match struct { Losses int `db:"losses" json:"losses"` WinRate int `db:"win_rate" json:"winRate"` } + +type GameType string + +const ( + GameTypeSFV GameType = "sfv" + GameTypeSF6 GameType = "sf6" + GameTypeT8 GameType = "t8" +) diff --git a/pkg/model/session.go b/pkg/model/session.go index 19fa94db..7e2be890 100644 --- a/pkg/model/session.go +++ b/pkg/model/session.go @@ -5,11 +5,13 @@ type Session struct { UserId string `db:"user_id" json:"userId"` UserName string `db:"user_name" json:"userName"` CreatedAt string `db:"created_at" json:"createdAt"` - LP int `db:"ending_lp" json:"lp"` - MR int `db:"ending_mr" json:"mr"` + LP int `db:"lp" json:"lp"` + MR int `db:"mr" json:"mr"` Matches []*Match `json:"matches"` MatchesWon int `db:"matches_won" json:"matchesWon"` MatchesLost int `db:"matches_lost" json:"matchesLost"` + EndingLP int `db:"ending_lp" json:"endingLp"` + EndingMR int `db:"ending_mr" json:"endingMr"` StartingLP int `db:"starting_lp" json:"startingLp"` StartingMR int `db:"starting_mr" json:"startingMr"` LPGain int `db:"lp_gain" json:"lpGain"` diff --git a/pkg/storage/sql/user.go b/pkg/storage/sql/user.go index 1e74b061..7f57de89 100644 --- a/pkg/storage/sql/user.go +++ b/pkg/storage/sql/user.go @@ -2,11 +2,11 @@ package sql import ( "context" - "fmt" "errors" + "fmt" - "github.com/jmoiron/sqlx" "database/sql" + "github.com/jmoiron/sqlx" "github.com/williamsjokvist/cfn-tracker/pkg/model" ) diff --git a/pkg/storage/txt/storage.go b/pkg/storage/txt/storage.go index 4ad954ae..05d25c6d 100644 --- a/pkg/storage/txt/storage.go +++ b/pkg/storage/txt/storage.go @@ -23,37 +23,37 @@ func NewStorage() (*Storage, error) { }, nil } -func (s *Storage) SaveTrackingState(ts *model.TrackingState) error { - err := s.saveTxtFile(`wins.txt`, strconv.Itoa(ts.Wins)) +func (s *Storage) SaveMatch(match model.Match) error { + err := s.saveTxtFile(`wins.txt`, strconv.Itoa(match.Wins)) if err != nil { return fmt.Errorf(`save wins txt: %w`, err) } - err = s.saveTxtFile(`losses.txt`, strconv.Itoa(ts.Losses)) + err = s.saveTxtFile(`losses.txt`, strconv.Itoa(match.Losses)) if err != nil { return fmt.Errorf(`save losses txt: %w`, err) } - err = s.saveTxtFile(`win-rate.txt`, strconv.Itoa(ts.WinRate)+`%`) + err = s.saveTxtFile(`win-rate.txt`, strconv.Itoa(match.WinRate)+`%`) if err != nil { return fmt.Errorf(`save win rate txt: %w`, err) } - err = s.saveTxtFile(`win-streak.txt`, strconv.Itoa(ts.WinStreak)) + err = s.saveTxtFile(`win-streak.txt`, strconv.Itoa(match.WinStreak)) if err != nil { return fmt.Errorf(`save win streak txt: %w`, err) } - err = s.saveTxtFile(`lp.txt`, strconv.Itoa(ts.LP)) + err = s.saveTxtFile(`lp.txt`, strconv.Itoa(match.LP)) if err != nil { return fmt.Errorf(`save lp txt: %w`, err) } - err = s.saveTxtFile(`mr.txt`, strconv.Itoa(ts.MR)) + err = s.saveTxtFile(`mr.txt`, strconv.Itoa(match.MR)) if err != nil { return fmt.Errorf(`save mr txt: %w`, err) } - lpGain := strconv.Itoa(ts.LPGain) - if ts.LPGain > 0 { + lpGain := strconv.Itoa(match.LPGain) + if match.LPGain > 0 { lpGain = `+` + lpGain } - mrGain := strconv.Itoa(ts.MRGain) - if ts.MRGain > 0 { + mrGain := strconv.Itoa(match.MRGain) + if match.MRGain > 0 { mrGain = `+` + mrGain } err = s.saveTxtFile(`lp-gain.txt`, lpGain) diff --git a/pkg/tracker/sf6/auth.go b/pkg/tracker/sf6/auth.go deleted file mode 100644 index 793698cd..00000000 --- a/pkg/tracker/sf6/auth.go +++ /dev/null @@ -1,84 +0,0 @@ -package sf6 - -import ( - "errors" - "fmt" - "log" - "math/rand" - "strconv" - "strings" - "time" - - "github.com/williamsjokvist/cfn-tracker/pkg/tracker" -) - -func (t *SF6Tracker) Authenticate(email string, password string, statChan chan tracker.AuthStatus) { - status := &tracker.AuthStatus{Progress: 0, Err: nil} - defer func() { - if r := recover(); r != nil { - log.Println(`Recovered from panic: `, r) - statChan <- *status.WithError(fmt.Errorf(`panic: %v`, r)) - } - }() - - if t.isAuthenticated || strings.Contains(t.Page.MustInfo().URL, `buckler`) { - t.isAuthenticated = true - return - } - - if email == "" || password == "" { - statChan <- *status.WithError(errors.New("missing credentials")) - return - } - - log.Println(`Logging in`) - t.Page.MustNavigate(`https://cid.capcom.com/ja/login/?guidedBy=web`).MustWaitLoad().MustWaitIdle() - statChan <- *status.WithProgress(10) - - log.Print("Checking if already authed") - if strings.Contains(t.Page.MustInfo().URL, `cid.capcom.com/ja/mypage`) { - log.Print("User already authed") - t.isAuthenticated = true - statChan <- *status.WithProgress(100) - return - } - log.Print("Not authed, continuing with auth process") - - // Bypass age check - if strings.Contains(t.Page.MustInfo().URL, `agecheck`) { - t.Page.MustElement(`#country`).MustSelect(COUNTRIES[rand.Intn(len(COUNTRIES))]) - t.Page.MustElement(`#birthYear`).MustSelect(strconv.Itoa(rand.Intn(1999-1970) + 1970)) - t.Page.MustElement(`#birthMonth`).MustSelect(strconv.Itoa(rand.Intn(12-1) + 1)) - t.Page.MustElement(`#birthDay`).MustSelect(strconv.Itoa(rand.Intn(28-1) + 1)) - t.Page.MustElement(`form button[type="submit"]`).MustClick() - t.Page.MustWaitLoad().MustWaitRequestIdle() - } - statChan <- *status.WithProgress(30) - - // Submit form - t.Page.MustElement(`input[name="email"]`).MustInput(email) - t.Page.MustElement(`input[name="password"]`).MustInput(password) - t.Page.MustElement(`button[type="submit"]`).MustClick() - statChan <- *status.WithProgress(50) - - // Wait for redirection - var secondsWaited time.Duration = 0 - for { - // Break out if we are no longer on Auth0 (redirected to CFN) - if !strings.Contains(t.Page.MustInfo().URL, `auth.cid.capcom.com`) { - break - } - - time.Sleep(time.Second) - secondsWaited += time.Second - log.Println(`Waiting for gateway to pass...`, secondsWaited) - } - statChan <- *status.WithProgress(65) - - t.Page.MustNavigate(`https://www.streetfighter.com/6/buckler/auth/loginep?redirect_url=/`) - t.Page.MustWaitLoad().MustWaitRequestIdle() - - statChan <- *status.WithProgress(100) - t.isAuthenticated = true - log.Println(`Authentication passed`) -} diff --git a/pkg/tracker/sf6/cfn/client.go b/pkg/tracker/sf6/cfn/client.go new file mode 100644 index 00000000..3258f00a --- /dev/null +++ b/pkg/tracker/sf6/cfn/client.go @@ -0,0 +1,129 @@ +package cfn + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/williamsjokvist/cfn-tracker/pkg/browser" + "github.com/williamsjokvist/cfn-tracker/pkg/tracker" +) + +type CFNClient interface { + GetBattleLog(cfn string) (*BattleLog, error) +} + +type Client struct { + browser *browser.Browser +} + +var _ CFNClient = (*Client)(nil) + +func NewCFNClient(browser *browser.Browser) *Client { + return &Client{browser} +} + +func (c *Client) GetBattleLog(cfn string) (*BattleLog, error) { + err := c.browser.Page.Navigate(fmt.Sprintf(`https://www.streetfighter.com/6/buckler/profile/%s/battlelog/rank`, cfn)) + if err != nil { + return nil, fmt.Errorf(`navigate to cfn: %w`, err) + } + err = c.browser.Page.WaitLoad() + if err != nil { + return nil, fmt.Errorf(`wait for cfn to load: %w`, err) + } + nextData, err := c.browser.Page.Element(`#__NEXT_DATA__`) + if err != nil { + return nil, fmt.Errorf(`get next_data element: %w`, err) + } + body, err := nextData.Text() + if err != nil { + return nil, fmt.Errorf(`get next_data json: %w`, err) + } + + var profilePage ProfilePage + err = json.Unmarshal([]byte(body), &profilePage) + if err != nil { + return nil, fmt.Errorf(`unmarshal battle log: %w`, err) + } + + bl := &profilePage.Props.PageProps + if bl.Common.StatusCode != 200 { + return nil, fmt.Errorf(`failed to fetch battle log, received status code %v`, bl.Common.StatusCode) + } + return bl, nil +} + +func (t *Client) Authenticate(email string, password string, statChan chan tracker.AuthStatus) { + status := &tracker.AuthStatus{Progress: 0, Err: nil} + defer func() { + if r := recover(); r != nil { + log.Println(`Recovered from panic: `, r) + statChan <- *status.WithError(fmt.Errorf(`panic: %v`, r)) + } + }() + + if strings.Contains(t.browser.Page.MustInfo().URL, `buckler`) { + statChan <- *status.WithProgress(100) + return + } + + if email == "" || password == "" { + statChan <- *status.WithError(errors.New("missing credentials")) + return + } + + log.Println(`Logging in`) + t.browser.Page.MustNavigate(`https://cid.capcom.com/ja/login/?guidedBy=web`).MustWaitLoad().MustWaitIdle() + statChan <- *status.WithProgress(10) + + log.Print("Checking if already authed") + if strings.Contains(t.browser.Page.MustInfo().URL, `cid.capcom.com/ja/mypage`) { + log.Print("User already authed") + statChan <- *status.WithProgress(100) + return + } + log.Print("Not authed, continuing with auth process") + + // Bypass age check + if strings.Contains(t.browser.Page.MustInfo().URL, `agecheck`) { + t.browser.Page.MustElement(`#country`).MustSelect(COUNTRIES[rand.Intn(len(COUNTRIES))]) + t.browser.Page.MustElement(`#birthYear`).MustSelect(strconv.Itoa(rand.Intn(1999-1970) + 1970)) + t.browser.Page.MustElement(`#birthMonth`).MustSelect(strconv.Itoa(rand.Intn(12-1) + 1)) + t.browser.Page.MustElement(`#birthDay`).MustSelect(strconv.Itoa(rand.Intn(28-1) + 1)) + t.browser.Page.MustElement(`form button[type="submit"]`).MustClick() + t.browser.Page.MustWaitLoad().MustWaitRequestIdle() + } + statChan <- *status.WithProgress(30) + + // Submit form + t.browser.Page.MustElement(`input[name="email"]`).MustInput(email) + t.browser.Page.MustElement(`input[name="password"]`).MustInput(password) + t.browser.Page.MustElement(`button[type="submit"]`).MustClick() + statChan <- *status.WithProgress(50) + + // Wait for redirection + var secondsWaited time.Duration = 0 + for { + // Break out if we are no longer on Auth0 (redirected to CFN) + if !strings.Contains(t.browser.Page.MustInfo().URL, `auth.cid.capcom.com`) { + break + } + + time.Sleep(time.Second) + secondsWaited += time.Second + log.Println(`Waiting for gateway to pass...`, secondsWaited) + } + statChan <- *status.WithProgress(65) + + t.browser.Page.MustNavigate(`https://www.streetfighter.com/6/buckler/auth/loginep?redirect_url=/`) + t.browser.Page.MustWaitLoad().MustWaitRequestIdle() + + statChan <- *status.WithProgress(100) + log.Println(`Authentication passed`) +} diff --git a/pkg/tracker/sf6/model.go b/pkg/tracker/sf6/cfn/model.go similarity index 99% rename from pkg/tracker/sf6/model.go rename to pkg/tracker/sf6/cfn/model.go index e65575a5..aab5f3d2 100644 --- a/pkg/tracker/sf6/model.go +++ b/pkg/tracker/sf6/cfn/model.go @@ -1,4 +1,4 @@ -package sf6 +package cfn import "strconv" diff --git a/pkg/tracker/sf6/track.go b/pkg/tracker/sf6/track.go index 5024dfef..a7f483b1 100644 --- a/pkg/tracker/sf6/track.go +++ b/pkg/tracker/sf6/track.go @@ -2,10 +2,7 @@ package sf6 import ( "context" - "encoding/json" - "errors" "fmt" - "log" "net/http" "time" @@ -17,237 +14,102 @@ import ( "github.com/williamsjokvist/cfn-tracker/pkg/storage/sql" "github.com/williamsjokvist/cfn-tracker/pkg/storage/txt" "github.com/williamsjokvist/cfn-tracker/pkg/tracker" + "github.com/williamsjokvist/cfn-tracker/pkg/tracker/sf6/cfn" "github.com/williamsjokvist/cfn-tracker/pkg/utils" ) type SF6Tracker struct { - isAuthenticated bool - stopPolling context.CancelFunc - state map[string]*model.TrackingState - sesh *model.Session - user *model.User - *browser.Browser - - sqlDb *sql.Storage - txtDb *txt.Storage + cfnClient *cfn.Client + sqlDb *sql.Storage + txtDb *txt.Storage } var _ tracker.GameTracker = (*SF6Tracker)(nil) func NewSF6Tracker(browser *browser.Browser, sqlDb *sql.Storage, txtDb *txt.Storage) *SF6Tracker { return &SF6Tracker{ - Browser: browser, - stopPolling: func() {}, - sqlDb: sqlDb, - txtDb: txtDb, - state: make(map[string]*model.TrackingState, 4), + cfnClient: cfn.NewCFNClient(browser), + sqlDb: sqlDb, + txtDb: txtDb, } } // Start will update the tracking state when new matches are played. -func (t *SF6Tracker) Start(ctx context.Context, userCode string, restore bool, pollRate time.Duration) error { - if !t.isAuthenticated { - log.Println(`tracker not authenticated`) - return errorsx.NewFormattedError(http.StatusUnauthorized, errors.New(`tracker not authenticated`)) - } - - startPolling := func() { - pollCtx, cancelFn := context.WithCancel(ctx) - t.stopPolling = cancelFn - go t.poll(pollCtx, userCode, pollRate) - } - +func (t *SF6Tracker) Init(ctx context.Context, userCode string, restore bool) (*model.Session, error) { if restore { - sesh, err := t.sqlDb.GetLatestSession(ctx, userCode) + session, err := t.sqlDb.GetLatestSession(ctx, userCode) if err != nil { - return errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf(`failed to get last session: %w`, err)) + return nil, errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf(`failed to get last session: %w`, err)) } - t.sesh = sesh - t.user, err = t.sqlDb.GetUserByCode(ctx, userCode) + _, err = t.sqlDb.GetUserByCode(ctx, userCode) if err != nil { - return errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf(`failed to get user: %w`, err)) + return nil, errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf(`failed to get user: %w`, err)) } - trackingState := t.getTrackingStateForLastMatch() - if trackingState == nil { - bl, err := t.fetchBattleLog(userCode) + if len(session.Matches) == 0 { + bl, err := t.cfnClient.GetBattleLog(userCode) if err != nil { - return errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to fetch battle log: %w`, err)) + return nil, errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to fetch battle log: %w`, err)) } - trackingState = &model.TrackingState{ + wails.EventsEmit(ctx, `cfn-data`, model.TrackingState{ CFN: bl.GetCFN(), LP: bl.GetLP(), MR: bl.GetMR(), Character: bl.GetCharacter(), - } + }) + + return session, nil + } + + lastMatch := *session.Matches[0] + if err := t.txtDb.SaveMatch(lastMatch); err != nil { + return nil, err } - wails.EventsEmit(ctx, `cfn-data`, trackingState) - startPolling() - return nil + wails.EventsEmit(ctx, `cfn-data`, lastMatch) + return session, nil } - bl, err := t.fetchBattleLog(userCode) + bl, err := t.cfnClient.GetBattleLog(userCode) if err != nil { - return errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to fetch battle log: %w`, err)) + return nil, errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to fetch battle log: %w`, err)) } - user := model.User{ + + err = t.sqlDb.SaveUser(ctx, model.User{ DisplayName: bl.GetCFN(), Code: userCode, - } - err = t.sqlDb.SaveUser(ctx, user) + }) if err != nil { - return errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to save user: %w`, err)) + return nil, errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to save user: %w`, err)) } - t.user = &user - sesh, err := t.sqlDb.CreateSession(ctx, userCode) + session, err := t.sqlDb.CreateSession(ctx, userCode) if err != nil { - return errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to create session: %w`, err)) + return nil, errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf(`failed to create session: %w`, err)) } - t.sesh = sesh // set starting LP so we don't count the first polled match - t.sesh.LP = bl.GetLP() - t.sesh.MR = bl.GetMR() + session.LP = bl.GetLP() + session.MR = bl.GetMR() wails.EventsEmit(ctx, `cfn-data`, model.TrackingState{ CFN: bl.GetCFN(), LP: bl.GetLP(), MR: bl.GetMR(), Character: bl.GetCharacter(), }) - - startPolling() - return nil + return session, nil } -func (t *SF6Tracker) poll(ctx context.Context, userCode string, pollRate time.Duration) { - i := 0 - retries := 0 - - didStop := func() bool { - return utils.SleepOrBreak(pollRate, func() bool { - select { - case <-ctx.Done(): - return true - default: - return false - } - }) - } - - for { - i++ - log.Println(`polling`, i) - - bl, err := t.fetchBattleLog(userCode) - if err != nil { - retries++ - log.Println(`failed to poll battle log: `, err, `(retry: `, retries, `)`) - if didStop() || retries > 5 { - wails.EventsEmit(ctx, `stopped-tracking`) - break - } - continue - } - - if didStop() { - wails.EventsEmit(ctx, `stopped-tracking`) - break - } - - err = t.updateSession(ctx, bl) - if err != nil { - log.Println(`failed to update session: `, err) - } - } -} - -func (t *SF6Tracker) updateSession(ctx context.Context, bl *BattleLog) error { - // no new match played - if t.sesh.LP == bl.GetLP() { - return nil - } - match := getNewestMatch(t.sesh, bl) - - t.sesh.LP = bl.GetLP() - t.sesh.MR = bl.GetMR() - t.sesh.Matches = append([]*model.Match{&match}, t.sesh.Matches...) - err := t.sqlDb.UpdateSession(ctx, t.sesh) - if err != nil { - return fmt.Errorf("failed to update session: %w", err) - } - if err := t.sqlDb.SaveMatch(ctx, match); err != nil { - return fmt.Errorf("failed to save match: %w", err) - } - trackingState := t.getTrackingStateForLastMatch() - if trackingState != nil { - trackingState.Log() - wails.EventsEmit(ctx, `cfn-data`, trackingState) - if err := t.txtDb.SaveTrackingState(trackingState); err != nil { - return fmt.Errorf("failed to save tracking state: %w", err) - } - } - - return nil -} - -func (t *SF6Tracker) getTrackingStateForLastMatch() *model.TrackingState { - if len(t.sesh.Matches) == 0 { - return nil - } - lastMatch := t.sesh.Matches[0] - return &model.TrackingState{ - UserCode: t.user.Code, - CFN: t.user.DisplayName, - Wins: lastMatch.Wins, - Losses: lastMatch.Losses, - WinRate: lastMatch.WinRate, - WinStreak: lastMatch.WinStreak, - MR: lastMatch.MR, - LP: lastMatch.LP, - LPGain: lastMatch.LPGain, - MRGain: lastMatch.MRGain, - Character: lastMatch.Character, - IsWin: lastMatch.Victory, - Opponent: lastMatch.Opponent, - OpponentCharacter: lastMatch.OpponentCharacter, - OpponentLP: lastMatch.OpponentLP, - OpponentLeague: lastMatch.OpponentLeague, - Date: lastMatch.Date, - TimeStamp: lastMatch.Time, - } -} - -func (t *SF6Tracker) fetchBattleLog(userCode string) (*BattleLog, error) { - err := t.Page.Navigate(fmt.Sprintf(`https://www.streetfighter.com/6/buckler/profile/%s/battlelog/rank`, userCode)) +func (t *SF6Tracker) Poll(ctx context.Context, cancel context.CancelFunc, session *model.Session, matchChan chan model.Match) { + bl, err := t.cfnClient.GetBattleLog(session.UserId) if err != nil { - return nil, fmt.Errorf(`navigate to cfn: %w`, err) + cancel() } - err = t.Page.WaitLoad() - if err != nil { - return nil, fmt.Errorf(`wait for cfn to load: %w`, err) - } - nextData, err := t.Page.Element(`#__NEXT_DATA__`) - if err != nil { - return nil, fmt.Errorf(`get next_data element: %w`, err) - } - body, err := nextData.Text() - if err != nil { - return nil, fmt.Errorf(`get next_data json: %w`, err) - } - - var profilePage ProfilePage - err = json.Unmarshal([]byte(body), &profilePage) - if err != nil { - return nil, fmt.Errorf(`unmarshal battle log: %w`, err) - } - - bl := &profilePage.Props.PageProps - if bl.Common.StatusCode != 200 { - return nil, fmt.Errorf(`failed to fetch battle log, received status code %v`, bl.Common.StatusCode) + // no new match played + if session.LP == bl.GetLP() { + return } - return bl, nil + matchChan <- getMatch(session, bl) } -func getOpponentInfo(myCfn string, replay *Replay) PlayerInfo { +func getOpponentInfo(myCfn string, replay *cfn.Replay) cfn.PlayerInfo { if myCfn == replay.Player1Info.Player.FighterID { return replay.Player2Info } else { @@ -255,7 +117,7 @@ func getOpponentInfo(myCfn string, replay *Replay) PlayerInfo { } } -func getNewestMatch(sesh *model.Session, bl *BattleLog) model.Match { +func getMatch(sesh *model.Session, bl *cfn.BattleLog) model.Match { latestReplay := bl.ReplayList[0] opponent := getOpponentInfo(bl.GetCFN(), &latestReplay) victory := !isVictory(opponent.RoundResults) @@ -319,12 +181,6 @@ func isVictory(roundResults []int) bool { return (roundsPlayed == 3 && len(losses) == 1) || len(losses) == 0 } -// Stop will stop any current trackingz -func (t *SF6Tracker) Stop() { - t.stopPolling() -} -func (t *SF6Tracker) ForcePoll() {} - func getLeagueFromLP(lp int) string { if lp >= 25000 { return `Master` @@ -344,3 +200,7 @@ func getLeagueFromLP(lp int) string { return `Rookie` } + +func (t *SF6Tracker) Authenticate(email string, password string, statChan chan tracker.AuthStatus) { + t.cfnClient.Authenticate(email, password, statChan) +} diff --git a/pkg/tracker/sfv/track.go b/pkg/tracker/sfv/track.go index e0a30dca..e187db0f 100644 --- a/pkg/tracker/sfv/track.go +++ b/pkg/tracker/sfv/track.go @@ -16,7 +16,7 @@ import ( "github.com/williamsjokvist/cfn-tracker/pkg/browser" "github.com/williamsjokvist/cfn-tracker/pkg/errorsx" "github.com/williamsjokvist/cfn-tracker/pkg/model" - "github.com/williamsjokvist/cfn-tracker/pkg/tracker" + _ "github.com/williamsjokvist/cfn-tracker/pkg/tracker" "github.com/williamsjokvist/cfn-tracker/pkg/utils" ) @@ -29,7 +29,7 @@ type SFVTracker struct { *browser.Browser } -var _ tracker.GameTracker = (*SFVTracker)(nil) +// var _ tracker.GameTracker = (*SFVTracker)(nil) func NewSFVTracker(browser *browser.Browser) *SFVTracker { return &SFVTracker{ @@ -51,14 +51,14 @@ func (t *SFVTracker) stopFn(ctx context.Context) { } // Start will update the MatchHistory when new matches are played. -func (t *SFVTracker) Start(ctx context.Context, cfn string, restoreData bool, refreshInterval time.Duration) error { +func (t *SFVTracker) Init(ctx context.Context, cfn string, restore bool) (*model.Session, error) { // safe guard if t.isTracking { - return nil + return nil, errors.New("tracking is already in progress") } if !t.isAuthenticated { - return errorsx.NewFormattedError(http.StatusUnauthorized, errors.New(`tracker not authenticated`)) + return nil, errorsx.NewFormattedError(http.StatusUnauthorized, errors.New(`tracker not authenticated`)) } t.mh = &model.TrackingState{ @@ -84,7 +84,7 @@ func (t *SFVTracker) Start(ctx context.Context, cfn string, restoreData bool, re isValidProfile := t.Page.MustHas(`.leagueInfo`) if !isValidProfile { t.stopFn(ctx) - return errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf(`failed to get user`)) + return nil, fmt.Errorf("failed to get user") } log.Println(`Profile loaded`) @@ -95,14 +95,12 @@ func (t *SFVTracker) Start(ctx context.Context, cfn string, restoreData bool, re pollCtx, cancel := context.WithCancel(ctx) t.stopTracking = cancel - go t.poll(pollCtx, cfn, refreshInterval) + go t.Poll(pollCtx, cfn, 30*time.Second) - return nil + return nil, nil } -func (t *SFVTracker) ForcePoll() {} - -func (t *SFVTracker) poll(ctx context.Context, cfn string, refreshInterval time.Duration) { +func (t *SFVTracker) Poll(ctx context.Context, cfn string, refreshInterval time.Duration) { for { didBreak := utils.SleepOrBreak(refreshInterval, func() bool { select { diff --git a/pkg/tracker/t8/track.go b/pkg/tracker/t8/track.go index 3fa9ba61..6a25161c 100644 --- a/pkg/tracker/t8/track.go +++ b/pkg/tracker/t8/track.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "log" "net/http" "time" @@ -19,59 +18,50 @@ import ( ) type T8Tracker struct { - cancel context.CancelFunc - forcePollChan chan struct{} - wavuClient *wavu.Client - sqlDb *sql.Storage - txtDb *txt.Storage + wavuClient *wavu.Client + sqlDb *sql.Storage + txtDb *txt.Storage } var _ tracker.GameTracker = (*T8Tracker)(nil) func NewT8Tracker(sqlDb *sql.Storage, txtDb *txt.Storage) *T8Tracker { return &T8Tracker{ - cancel: func() {}, - forcePollChan: nil, - sqlDb: sqlDb, - txtDb: txtDb, - wavuClient: wavu.NewClient(), + wavuClient: wavu.NewClient(), + sqlDb: sqlDb, + txtDb: txtDb, } } -func (t *T8Tracker) Start(ctx context.Context, polarisId string, restore bool, pollRate time.Duration) error { +func (t *T8Tracker) Init(ctx context.Context, polarisId string, restore bool) (*model.Session, error) { if restore { session, err := t.sqlDb.GetLatestSession(ctx, polarisId) if err != nil { - return errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf("get last session: %w", err)) + return nil, errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf("get last session: %w", err)) } if len(session.Matches) > 0 { - lastMatch := session.Matches[0] - trackingState := model.ConvMatchToTrackingState(*lastMatch) - wails.EventsEmit(ctx, "cfn-data", trackingState) + wails.EventsEmit(ctx, "cfn-data", *session.Matches[0]) } - go t.poll(ctx, session, pollRate) - return nil + return session, nil } user, err := t.sqlDb.GetUserByCode(ctx, polarisId) if err != nil && !errors.Is(err, sql.ErrUserNotFound) { - return errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf("get user: %w", err)) + return nil, errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf("get user: %w", err)) } if user == nil { if err := t.createUser(ctx, polarisId); err != nil { - return errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf("create user: %w", err)) + return nil, errorsx.NewFormattedError(http.StatusNotFound, fmt.Errorf("create user: %w", err)) } } session, err := t.sqlDb.CreateSession(ctx, polarisId) if err != nil { - return errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf("create session: %w", err)) + return nil, errorsx.NewFormattedError(http.StatusInternalServerError, fmt.Errorf("create session: %w", err)) } - - go t.poll(ctx, session, pollRate) - return nil + return session, nil } func (t *T8Tracker) createUser(ctx context.Context, polarisId string) error { @@ -93,40 +83,10 @@ func (t *T8Tracker) createUser(ctx context.Context, polarisId string) error { return nil } -func (t *T8Tracker) poll(ctx context.Context, session *model.Session, pollRate time.Duration) { - // todo: more sophisticated poll rate - ticker := time.NewTicker(pollRate) - defer func() { - ticker.Stop() - close(t.forcePollChan) - t.forcePollChan = nil - wails.EventsEmit(ctx, "stopped-tracking") - }() - - t.forcePollChan = make(chan struct{}) - pollCtx, cancelFn := context.WithCancel(ctx) - t.cancel = cancelFn - - log.Println("polling") - t.pollFn(ctx, session) - for { - select { - case <-t.forcePollChan: - log.Println("forced poll") - t.pollFn(ctx, session) - case <-ticker.C: - log.Println("polling") - t.pollFn(ctx, session) - case <-pollCtx.Done(): - return - } - } -} - -func (t *T8Tracker) pollFn(ctx context.Context, session *model.Session) { +func (t *T8Tracker) Poll(ctx context.Context, cancel context.CancelFunc, session *model.Session, matchChan chan model.Match) { lastReplay, err := t.wavuClient.GetLastReplay(session.UserId) if err != nil { - t.Stop() + cancel() } var prevMatch *model.Match if len(session.Matches) > 0 { @@ -139,30 +99,7 @@ func (t *T8Tracker) pollFn(ctx context.Context, session *model.Session) { if match.SessionId == 0 { match.SessionId = session.Id } - session.Matches = append([]*model.Match{&match}, session.Matches...) - if err := t.sqlDb.SaveMatch(ctx, match); err != nil { - t.Stop() - } - - trackingState := model.ConvMatchToTrackingState(match) - wails.EventsEmit(ctx, "cfn-data", trackingState) - if err := t.txtDb.SaveTrackingState(&trackingState); err != nil { - t.Stop() - } -} - -func (t *T8Tracker) ForcePoll() { - if t.forcePollChan != nil { - t.forcePollChan <- struct{}{} - } -} - -func (t *T8Tracker) Stop() { - t.cancel() -} - -func (t *T8Tracker) Authenticate(email string, password string, statChan chan tracker.AuthStatus) { - statChan <- tracker.AuthStatus{Progress: 100, Err: nil} + matchChan <- match } func getMatch(wm *wavu.Replay, prevMatch *model.Match, p2 bool) model.Match { @@ -227,3 +164,7 @@ func getMatch(wm *wavu.Replay, prevMatch *model.Match, p2 bool) model.Match { Time: battleAt.Format("15:04"), } } + +func (t *T8Tracker) Authenticate(email string, password string, statChan chan tracker.AuthStatus) { + statChan <- tracker.AuthStatus{Progress: 100, Err: nil} +} diff --git a/pkg/tracker/tracker.go b/pkg/tracker/tracker.go index 2bd3a66c..21925afc 100644 --- a/pkg/tracker/tracker.go +++ b/pkg/tracker/tracker.go @@ -2,14 +2,14 @@ package tracker import ( "context" - "time" + + "github.com/williamsjokvist/cfn-tracker/pkg/model" ) type GameTracker interface { - Start(ctx context.Context, cfn string, restore bool, refreshInterval time.Duration) error + Init(ctx context.Context, polarisId string, restore bool) (*model.Session, error) + Poll(ctx context.Context, cancel context.CancelFunc, session *model.Session, matchChan chan model.Match) Authenticate(email string, password string, statusChan chan AuthStatus) - Stop() - ForcePoll() } type AuthStatus struct { @@ -26,26 +26,3 @@ func (s *AuthStatus) WithError(err error) *AuthStatus { s.Err = err return s } - -type GameType uint8 - -const ( - GameTypeUndefined GameType = iota - GameTypeSFV - GameTypeSF6 - GameTypeT8 -) - -func (s GameType) String() string { - switch s { - case GameTypeSFV: - return `sfv` - case GameTypeSF6: - return `sf6` - case GameTypeT8: - return `t8` - case GameTypeUndefined: - return `undefined` - } - return `unknown` -}