From 7b5a2145c318fe5edd80cdcbf7824d847f6b1241 Mon Sep 17 00:00:00 2001 From: Martin Raymond Date: Sat, 9 Mar 2024 08:59:06 -0800 Subject: [PATCH] dynamic tables (#14) --- apps/api/.env.example | 1 + apps/api/core.go | 13 +- apps/dashboard/src/app/app.module.ts | 8 + apps/dashboard/src/app/core/tables/index.ts | 4 + .../src/app/core/tables/tables.actions.ts | 11 + .../src/app/core/tables/tables.effects.ts | 27 ++ .../src/app/core/tables/tables.reducer.ts | 24 ++ .../src/app/core/tables/tables.selectors.ts | 9 + .../containers/home/home-page.component.html | 52 ++-- .../containers/home/home-page.component.ts | 9 +- .../src/app/services/table.service.ts | 26 ++ .../tournament-loaded.component.html | 6 +- .../tournament-loaded.component.ts | 1 - .../tournament-loaded.store.ts | 7 +- .../tournament-setup.component.html | 14 +- .../tournament-setup.store.ts | 9 +- libs/go/api/errors.go | 2 + libs/go/api/game.go | 294 ++++++++++++++---- libs/go/api/overlay.go | 110 +++++-- libs/go/api/routes.go | 61 ++-- libs/go/api/tables.go | 120 ++++++- libs/go/apidocs/overlay-toggle_swagger.go | 14 +- libs/go/apidocs/table-count_swagger.go | 17 + libs/go/apidocs/table_swagger.go | 30 ++ package-lock.json | 22 ++ package.json | 1 + 26 files changed, 715 insertions(+), 177 deletions(-) create mode 100644 apps/dashboard/src/app/core/tables/index.ts create mode 100644 apps/dashboard/src/app/core/tables/tables.actions.ts create mode 100644 apps/dashboard/src/app/core/tables/tables.effects.ts create mode 100644 apps/dashboard/src/app/core/tables/tables.reducer.ts create mode 100644 apps/dashboard/src/app/core/tables/tables.selectors.ts create mode 100644 apps/dashboard/src/app/services/table.service.ts create mode 100644 libs/go/apidocs/table-count_swagger.go create mode 100644 libs/go/apidocs/table_swagger.go diff --git a/apps/api/.env.example b/apps/api/.env.example index 5ac51c6..e38560d 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -5,3 +5,4 @@ POSTGRES_DB=pool POSTGRES_PORT=5432 CHALLONGE_API_KEY=secret CHALLONGE_USERNAME=username +MAX_TABLE_COUNT=11 diff --git a/apps/api/core.go b/apps/api/core.go index 1dcb120..90a239e 100644 --- a/apps/api/core.go +++ b/apps/api/core.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "strconv" "github.com/codephobia/pool-overlay/libs/go/api" "github.com/codephobia/pool-overlay/libs/go/challonge" @@ -47,9 +48,15 @@ func NewCore() (*Core, error) { // Initialize game state. tables := map[int]*state.State{} - tables[1] = state.NewState(db, 1) - tables[2] = state.NewState(db, 2) - tables[3] = state.NewState(db, 3) + + // Add tables to game state based on default provided in env file. + maxTableCount, err := strconv.Atoi(os.Getenv("MAX_TABLE_COUNT")) + if err != nil { + maxTableCount = 3 + } + for i := 1; i <= maxTableCount; i++ { + tables[i] = state.NewState(db, i) + } // Initialize Challonge. challonge := challonge.NewChallonge(os.Getenv("CHALLONGE_API_KEY"), os.Getenv("CHALLONGE_USERNAME"), db, overlay, tables) diff --git a/apps/dashboard/src/app/app.module.ts b/apps/dashboard/src/app/app.module.ts index 4d5a20e..abf3365 100644 --- a/apps/dashboard/src/app/app.module.ts +++ b/apps/dashboard/src/app/app.module.ts @@ -1,8 +1,10 @@ import { NgModule, isDevMode } from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; import { StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; @@ -11,6 +13,7 @@ import { AppComponent } from './components/app/app.component'; import { SideNavComponent } from './components/side-nav/side-nav.component'; import { ENV_CONFIG } from './models/environment-config.model'; import { environment } from '../environments/environment'; +import * as fromTables from './core/tables'; const COMPONENTS = [ AppComponent, @@ -24,11 +27,16 @@ const COMPONENTS = [ imports: [ BrowserModule, BrowserAnimationsModule, + HttpClientModule, FontAwesomeModule, AppRoutingModule, StoreModule.forRoot({ + [fromTables.stateKey]: fromTables.reducer, router: routerReducer, }), + EffectsModule.forRoot([ + fromTables.TablesEffects, + ]), StoreRouterConnectingModule.forRoot(), StoreDevtoolsModule.instrument({ maxAge: 25, diff --git a/apps/dashboard/src/app/core/tables/index.ts b/apps/dashboard/src/app/core/tables/index.ts new file mode 100644 index 0000000..129148e --- /dev/null +++ b/apps/dashboard/src/app/core/tables/index.ts @@ -0,0 +1,4 @@ +export * from './tables.actions'; +export * from './tables.effects'; +export * from './tables.reducer'; +export * from './tables.selectors'; diff --git a/apps/dashboard/src/app/core/tables/tables.actions.ts b/apps/dashboard/src/app/core/tables/tables.actions.ts new file mode 100644 index 0000000..2fb255a --- /dev/null +++ b/apps/dashboard/src/app/core/tables/tables.actions.ts @@ -0,0 +1,11 @@ +import { createActionGroup, emptyProps, props } from '@ngrx/store'; + +export const TablesActions = createActionGroup({ + source: 'Tables', + events: { + 'Get Count': emptyProps(), + 'Get Count Success': props<{ count: number }>(), + 'Get Count Error': props<{ error: string }>(), + 'Set Count': props<{ count: number }>(), + }, +}); diff --git a/apps/dashboard/src/app/core/tables/tables.effects.ts b/apps/dashboard/src/app/core/tables/tables.effects.ts new file mode 100644 index 0000000..241fbd4 --- /dev/null +++ b/apps/dashboard/src/app/core/tables/tables.effects.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { Actions, ROOT_EFFECTS_INIT, createEffect, ofType } from '@ngrx/effects'; + +import { TablesActions } from './tables.actions'; +import { TableService } from '../../services/table.service'; +import { catchError, map, of, switchMap } from 'rxjs'; + +@Injectable() +export class TablesEffects { + constructor( + private actions$: Actions, + private tableService: TableService, + ) { } + + init$ = createEffect(() => this.actions$.pipe( + ofType(ROOT_EFFECTS_INIT), + map(() => TablesActions.getCount()), + )); + + getCount$ = createEffect(() => this.actions$.pipe( + ofType(TablesActions.getCount), + switchMap(() => this.tableService.count().pipe( + map(({ count }) => TablesActions.getCountSuccess({ count })), + catchError((error) => of(TablesActions.getCountError({ error }))), + )), + )); +} diff --git a/apps/dashboard/src/app/core/tables/tables.reducer.ts b/apps/dashboard/src/app/core/tables/tables.reducer.ts new file mode 100644 index 0000000..0ee180e --- /dev/null +++ b/apps/dashboard/src/app/core/tables/tables.reducer.ts @@ -0,0 +1,24 @@ +import { createReducer, on } from '@ngrx/store'; +import { TablesActions } from './tables.actions'; + +export const stateKey = 'table-count'; + +export interface State { + count: number; +} + +export const initialState: State = { + count: 1, +} + +export const reducer = createReducer( + initialState, + on( + TablesActions.getCountSuccess, + TablesActions.setCount, + (state, { count }) => ({ + ...state, + count, + }) + ) +); diff --git a/apps/dashboard/src/app/core/tables/tables.selectors.ts b/apps/dashboard/src/app/core/tables/tables.selectors.ts new file mode 100644 index 0000000..639647a --- /dev/null +++ b/apps/dashboard/src/app/core/tables/tables.selectors.ts @@ -0,0 +1,9 @@ +import { createFeatureSelector, createSelector } from '@ngrx/store'; +import { State, stateKey } from './tables.reducer'; + +export const selectTablesState = createFeatureSelector(stateKey); + +export const selectTablesCount = createSelector( + selectTablesState, + (state: State) => state.count +); diff --git a/apps/dashboard/src/app/overlay/containers/home/home-page.component.html b/apps/dashboard/src/app/overlay/containers/home/home-page.component.html index e8a7023..55cf499 100644 --- a/apps/dashboard/src/app/overlay/containers/home/home-page.component.html +++ b/apps/dashboard/src/app/overlay/containers/home/home-page.component.html @@ -1,25 +1,27 @@ - -
- - - -
-
- - - -
+ + +
+ + + +
+
+ + + +
+
diff --git a/apps/dashboard/src/app/overlay/containers/home/home-page.component.ts b/apps/dashboard/src/app/overlay/containers/home/home-page.component.ts index 935e371..409c6fc 100644 --- a/apps/dashboard/src/app/overlay/containers/home/home-page.component.ts +++ b/apps/dashboard/src/app/overlay/containers/home/home-page.component.ts @@ -1,5 +1,8 @@ import { Component } from '@angular/core'; import { faPlus } from '@fortawesome/pro-regular-svg-icons'; +import { Store } from '@ngrx/store'; +import * as fromTables from '../../../core/tables'; +import { map } from 'rxjs'; @Component({ selector: 'pool-overlay-home-page', @@ -7,8 +10,12 @@ import { faPlus } from '@fortawesome/pro-regular-svg-icons'; }) export class HomePageComponent { public faPlus = faPlus; - public tables: number[] = [1, 2, 3]; public currentTable = 1; + public tables$ = this.store.select(fromTables.selectTablesCount).pipe( + map((count) => Array.from(new Array(count), (x, i) => i + 1)), + ); + + constructor(private store: Store) { } public setTable(table: number): void { this.currentTable = table; diff --git a/apps/dashboard/src/app/services/table.service.ts b/apps/dashboard/src/app/services/table.service.ts new file mode 100644 index 0000000..8483696 --- /dev/null +++ b/apps/dashboard/src/app/services/table.service.ts @@ -0,0 +1,26 @@ +import { Inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { EnvironmentConfig, ENV_CONFIG } from '../models/environment-config.model'; +import { ICount } from '../models/count.model'; + +@Injectable({ providedIn: 'root' }) +export class TableService { + private apiURL: string; + private apiVersion: string; + private endpoint = 'table'; + + constructor( + @Inject(ENV_CONFIG) config: EnvironmentConfig, + private http: HttpClient, + ) { + this.apiURL = config.environment.apiURL; + this.apiVersion = config.environment.apiVersion; + } + + public count(): Observable { + let url = `${this.apiURL}/${this.apiVersion}/${this.endpoint}/count`; + return this.http.get(url); + } +} diff --git a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.html b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.html index 85fd763..b9305a3 100644 --- a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.html +++ b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.html @@ -10,8 +10,8 @@

{{ vm.tournament?.name }}

-
-
+
+
Table {{ table }}
@@ -20,7 +20,7 @@

{{ vm.tournament?.name }}

- + diff --git a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.ts b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.ts index 077c7d7..e200339 100644 --- a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.ts +++ b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.component.ts @@ -15,7 +15,6 @@ export class TournamentLoadedComponent { readonly faLock = faLock; readonly faEllipsisVertical = faEllipsisVertical readonly gameType = GameType; - readonly tables = [1, 2, 3]; readonly vm$ = this.store.vm$; constructor( diff --git a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.store.ts b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.store.ts index 96d6784..54440dc 100644 --- a/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.store.ts +++ b/apps/dashboard/src/app/tournament/components/tournament-loaded/tournament-loaded.store.ts @@ -5,6 +5,8 @@ import { IGame, OverlayState, Tournament } from '@pool-overlay/models'; import { switchMap, tap } from 'rxjs'; import { TablesService } from '../../services/tables.service'; import { TournamentsService } from '../../services/tournament.service'; +import { Store } from '@ngrx/store'; +import * as fromTables from '../../../core/tables'; export enum LoadingState { INIT, @@ -33,6 +35,7 @@ export const initialState: TournamentLoadedState = { export class TournamentLoadedStore extends ComponentStore { constructor( private router: Router, + private store: Store, private tournamentsService: TournamentsService, private tablesService: TablesService, ) { @@ -81,10 +84,12 @@ export class TournamentLoadedStore extends ComponentStore this.isLoaded$, this.tournament$, this.tables$, - (isLoaded, tournament, tables) => ({ + this.store.select(fromTables.selectTablesCount), + (isLoaded, tournament, tables, tablesCount) => ({ isLoaded, tournament, tables, + tablesArr: Array.from(new Array(tablesCount), (x, i) => i + 1), }) ); diff --git a/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.component.html b/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.component.html index cc7e40e..0c2e458 100644 --- a/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.component.html +++ b/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.component.html @@ -61,10 +61,16 @@

{{ vm.tournament?.name }}

- - - - + + +
diff --git a/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.store.ts b/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.store.ts index 33be6c8..0a4fdec 100644 --- a/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.store.ts +++ b/apps/dashboard/src/app/tournament/components/tournament-setup/tournament-setup.store.ts @@ -1,9 +1,11 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { GameType, Tournament } from '@pool-overlay/models'; import { switchMap, tap, withLatestFrom } from 'rxjs'; +import { GameType, Tournament } from '@pool-overlay/models'; import { TournamentsService } from '../../services/tournament.service'; +import * as fromTables from '../../../core/tables'; export enum LoadingState { INIT, @@ -43,6 +45,7 @@ export const initialState: TournamentSetupState = { export class TournamentSetupStore extends ComponentStore { constructor( private router: Router, + private store: Store, private tournamentsService: TournamentsService, ) { super(initialState); @@ -120,6 +123,7 @@ export class TournamentSetupStore extends ComponentStore { readonly vm$ = this.select( this.isLoaded$, this.tournament$, + this.store.select(fromTables.selectTablesCount), this.maxTables$, this.isHandicapped$, this.showOverlay$, @@ -129,9 +133,10 @@ export class TournamentSetupStore extends ComponentStore { this.gameType$, this.aSideRaceTo$, this.bSideRaceTo$, - (isLoaded, tournament, maxTables, isHandicapped, showOverlay, showFlags, showFargo, showScore, gameType, aSideRaceTo, bSideRaceTo) => ({ + (isLoaded, tournament, tablesCount, maxTables, isHandicapped, showOverlay, showFlags, showFargo, showScore, gameType, aSideRaceTo, bSideRaceTo) => ({ isLoaded, tournament, + tables: Array.from(new Array(tablesCount), (x, i) => i + 1), maxTables, isHandicapped, showOverlay, diff --git a/libs/go/api/errors.go b/libs/go/api/errors.go index 32c44bc..82da1d7 100644 --- a/libs/go/api/errors.go +++ b/libs/go/api/errors.go @@ -37,4 +37,6 @@ var ( ErrInvalidTournamentDetails = errors.New("invalid tournament details") // ErrInvalidTableNumber - Invalid table number. ErrInvalidTableNumber = errors.New("invalid table number") + // ErrRemoveOnlyTable - Cannot remove only table. + ErrRemoveOnlyTable = errors.New("cannot remove only table") ) diff --git a/libs/go/api/game.go b/libs/go/api/game.go index 1044e72..f4faa55 100644 --- a/libs/go/api/game.go +++ b/libs/go/api/game.go @@ -5,10 +5,12 @@ import ( "errors" "log" "net/http" + "strconv" "github.com/codephobia/pool-overlay/libs/go/events" "github.com/codephobia/pool-overlay/libs/go/models" "github.com/codephobia/pool-overlay/libs/go/overlay" + "github.com/gorilla/mux" ) const ( @@ -110,15 +112,15 @@ type GameFargoHotHandicapPatchResp struct { } // Handler for /game. -func (server *Server) handleGame(table int) http.Handler { +func (server *Server) handleGame() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "OPTIONS": server.HandleOptions(w, r) case "GET": - server.handleGameGet(w, r, table) + server.handleGameGet(w, r) case "POST": - server.handleGamePost(w, r, table) + server.handleGamePost(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -126,38 +128,70 @@ func (server *Server) handleGame(table int) http.Handler { } // Game handler for GET method. -func (server *Server) handleGameGet(w http.ResponseWriter, r *http.Request, table int) { +func (server *Server) handleGameGet(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + // send response - server.handleSuccess(w, r, server.tables[table].Game) + server.handleSuccess(w, r, server.tables[tableNum].Game) } // Game handler for POST method. -func (server *Server) handleGamePost(w http.ResponseWriter, r *http.Request, table int) { +func (server *Server) handleGamePost(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + // Save existing game. - if err := server.tables[table].Game.Save(true); err != nil { + if err := server.tables[tableNum].Game.Save(true); err != nil { server.handleError(w, r, http.StatusInternalServerError, err) return } // Check if we're in tournament mode right now. if server.challonge.InTournamentMode() { - if err := server.challonge.Continue(table); err != nil { + if err := server.challonge.Continue(tableNum); err != nil { log.Printf("unable to continue tournament: %s", err) } } else { // Reset game to create a new one with same players / settings. - if err := server.tables[table].Game.Reset(); err != nil { + if err := server.tables[tableNum].Game.Reset(); err != nil { server.handleError(w, r, http.StatusInternalServerError, err) return } } - // TODO: BROADCAST NEXT GAME FOR TABLE OR LOCK DOWN TABLET ON THAT TABLE - // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.GameEventType, - events.NewGameEventPayload(server.tables[table].Game), + events.NewGameEventPayload(server.tables[tableNum].Game), ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -172,13 +206,13 @@ func (server *Server) handleGamePost(w http.ResponseWriter, r *http.Request, tab } // Handler for /game/type. -func (server *Server) handleGameType(table int) http.Handler { +func (server *Server) handleGameType() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "OPTIONS": server.HandleOptions(w, r) case "PATCH": - server.handleGameTypePatch(w, r, table) + server.handleGameTypePatch(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -186,7 +220,24 @@ func (server *Server) handleGameType(table int) http.Handler { } // Game type handler for PATCH method. -func (server *Server) handleGameTypePatch(w http.ResponseWriter, r *http.Request, table int) { +func (server *Server) handleGameTypePatch(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + // decode the body var body GameTypePatchBody decoder := json.NewDecoder(r.Body) @@ -202,7 +253,7 @@ func (server *Server) handleGameTypePatch(w http.ResponseWriter, r *http.Request } // update game type - if err := server.tables[table].Game.SetType(body.Type); err != nil { + if err := server.tables[tableNum].Game.SetType(body.Type); err != nil { // TODO: LOG THIS ERROR server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError) } @@ -210,7 +261,7 @@ func (server *Server) handleGameTypePatch(w http.ResponseWriter, r *http.Request // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.GameEventType, - events.NewGameEventPayload(server.tables[table].Game), + events.NewGameEventPayload(server.tables[tableNum].Game), ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -225,13 +276,13 @@ func (server *Server) handleGameTypePatch(w http.ResponseWriter, r *http.Request } // Handler for /game/vs-mode. -func (server *Server) handleGameVsMode(table int) http.Handler { +func (server *Server) handleGameVsMode() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "OPTIONS": server.HandleOptions(w, r) case "PATCH": - server.handleGameVsModePatch(w, r, table) + server.handleGameVsModePatch(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -239,7 +290,24 @@ func (server *Server) handleGameVsMode(table int) http.Handler { } // Game vs-mode handler for PATCH method. -func (server *Server) handleGameVsModePatch(w http.ResponseWriter, r *http.Request, table int) { +func (server *Server) handleGameVsModePatch(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + // decode the body var body GameVsModePatchBody decoder := json.NewDecoder(r.Body) @@ -255,7 +323,7 @@ func (server *Server) handleGameVsModePatch(w http.ResponseWriter, r *http.Reque } // update game vs mode - if err := server.tables[table].Game.SetVsMode(body.VsMode); err != nil { + if err := server.tables[tableNum].Game.SetVsMode(body.VsMode); err != nil { // TODO: LOG THIS ERROR server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError) } @@ -263,7 +331,7 @@ func (server *Server) handleGameVsModePatch(w http.ResponseWriter, r *http.Reque // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.GameEventType, - events.NewGameEventPayload(server.tables[table].Game), + events.NewGameEventPayload(server.tables[tableNum].Game), ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -278,13 +346,13 @@ func (server *Server) handleGameVsModePatch(w http.ResponseWriter, r *http.Reque } // Handler for /game/race-to. -func (server *Server) handleGameRaceTo(table int) http.Handler { +func (server *Server) handleGameRaceTo() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "OPTIONS": server.HandleOptions(w, r) case "PATCH": - server.handleGameRaceToPatch(w, r, table) + server.handleGameRaceToPatch(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -292,7 +360,24 @@ func (server *Server) handleGameRaceTo(table int) http.Handler { } // Game race-to handler for PATCH method. -func (server *Server) handleGameRaceToPatch(w http.ResponseWriter, r *http.Request, table int) { +func (server *Server) handleGameRaceToPatch(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + // decode the body var body GameRaceToPatchBody decoder := json.NewDecoder(r.Body) @@ -309,12 +394,12 @@ func (server *Server) handleGameRaceToPatch(w http.ResponseWriter, r *http.Reque // update race to number if body.Direction == gameDirectionIncrement { - if err := server.tables[table].Game.IncrementRaceTo(); err != nil { + if err := server.tables[tableNum].Game.IncrementRaceTo(); err != nil { // TODO: LOG THIS ERROR server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError) } } else { - if err := server.tables[table].Game.DecrementRaceTo(); err != nil { + if err := server.tables[tableNum].Game.DecrementRaceTo(); err != nil { // TODO: LOG THIS ERROR server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError) } @@ -323,7 +408,7 @@ func (server *Server) handleGameRaceToPatch(w http.ResponseWriter, r *http.Reque // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.GameEventType, - events.NewGameEventPayload(server.tables[table].Game), + events.NewGameEventPayload(server.tables[tableNum].Game), ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -335,21 +420,21 @@ func (server *Server) handleGameRaceToPatch(w http.ResponseWriter, r *http.Reque // send response server.handleSuccess(w, r, GameRaceToResp{ - RaceTo: server.tables[table].Game.RaceTo, - UseFargoHotHandicap: server.tables[table].Game.UseFargoHotHandicap, + RaceTo: server.tables[tableNum].Game.RaceTo, + UseFargoHotHandicap: server.tables[tableNum].Game.UseFargoHotHandicap, }) } // Handler for /game/score -func (server *Server) handleGameScore(table int) http.Handler { +func (server *Server) handleGameScore() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "OPTIONS": server.HandleOptions(w, r) case "PATCH": - server.handleGameScorePatch(w, r, table) + server.handleGameScorePatch(w, r) case "DELETE": - server.handleGameScoreDelete(w, r, table) + server.handleGameScoreDelete(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -357,7 +442,24 @@ func (server *Server) handleGameScore(table int) http.Handler { } // Game score handler for PATCH method. -func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Request, table int) { +func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + // decode the body var body GameScorePatchBody decoder := json.NewDecoder(r.Body) @@ -374,7 +476,7 @@ func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Reques // update score if body.Direction == gameDirectionIncrement { - if err := server.tables[table].Game.IncrementScore(body.PlayerNum); err != nil { + if err := server.tables[tableNum].Game.IncrementScore(body.PlayerNum); err != nil { if errors.Is(err, models.ErrInvalidPlayerNumber) { server.handleError(w, r, http.StatusUnprocessableEntity, err) } else { @@ -384,7 +486,7 @@ func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Reques return } } else { - if err := server.tables[table].Game.DecrementScore(body.PlayerNum); err != nil { + if err := server.tables[tableNum].Game.DecrementScore(body.PlayerNum); err != nil { if errors.Is(err, models.ErrInvalidPlayerNumber) { server.handleError(w, r, http.StatusUnprocessableEntity, err) } else { @@ -398,7 +500,7 @@ func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Reques // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.GameEventType, - events.NewGameEventPayload(server.tables[table].Game), + events.NewGameEventPayload(server.tables[tableNum].Game), ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -410,7 +512,7 @@ func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Reques // Check if we're in tournament mode right now. if server.challonge.InTournamentMode() { - if err := server.challonge.UpdateMatchScore(table); err != nil { + if err := server.challonge.UpdateMatchScore(tableNum); err != nil { // fail gracefully since live score keeping isn't that important log.Printf("error updating match score on challonge: %s", err) } @@ -418,15 +520,32 @@ func (server *Server) handleGameScorePatch(w http.ResponseWriter, r *http.Reques // send response server.handleSuccess(w, r, GameScoreResp{ - ScoreOne: server.tables[table].Game.ScoreOne, - ScoreTwo: server.tables[table].Game.ScoreTwo, + ScoreOne: server.tables[tableNum].Game.ScoreOne, + ScoreTwo: server.tables[tableNum].Game.ScoreTwo, }) } // Game score reset handler for DELETE method. -func (server *Server) handleGameScoreDelete(w http.ResponseWriter, r *http.Request, table int) { +func (server *Server) handleGameScoreDelete(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + // reset game score - if err := server.tables[table].Game.ResetScore(); err != nil { + if err := server.tables[tableNum].Game.ResetScore(); err != nil { // TODO: LOG THIS ERROR server.handleError(w, r, http.StatusInternalServerError, ErrInternalServerError) } @@ -434,7 +553,7 @@ func (server *Server) handleGameScoreDelete(w http.ResponseWriter, r *http.Reque // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.GameEventType, - events.NewGameEventPayload(server.tables[table].Game), + events.NewGameEventPayload(server.tables[tableNum].Game), ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -446,21 +565,21 @@ func (server *Server) handleGameScoreDelete(w http.ResponseWriter, r *http.Reque // send response server.handleSuccess(w, r, GameScoreResp{ - ScoreOne: server.tables[table].Game.ScoreOne, - ScoreTwo: server.tables[table].Game.ScoreTwo, + ScoreOne: server.tables[tableNum].Game.ScoreOne, + ScoreTwo: server.tables[tableNum].Game.ScoreTwo, }) } // Handler for /game/players -func (server *Server) handleGamePlayers(table int) http.Handler { +func (server *Server) handleGamePlayers() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "OPTIONS": server.HandleOptions(w, r) case "PATCH": - server.handleGamePlayersPatch(w, r, table) + server.handleGamePlayersPatch(w, r) case "DELETE": - server.handleGamePlayersDelete(w, r, table) + server.handleGamePlayersDelete(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -468,7 +587,24 @@ func (server *Server) handleGamePlayers(table int) http.Handler { } // Game players handler for PATCH method. -func (server *Server) handleGamePlayersPatch(w http.ResponseWriter, r *http.Request, table int) { +func (server *Server) handleGamePlayersPatch(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + // decode the body var body GamePlayersPatchBody decoder := json.NewDecoder(r.Body) @@ -490,7 +626,7 @@ func (server *Server) handleGamePlayersPatch(w http.ResponseWriter, r *http.Requ } } - if err := server.tables[table].Game.SetPlayer(body.PlayerNum, &player); err != nil { + if err := server.tables[tableNum].Game.SetPlayer(body.PlayerNum, &player); err != nil { if errors.Is(err, models.ErrInvalidPlayerNumber) { server.handleError(w, r, http.StatusUnprocessableEntity, models.ErrInvalidPlayerNumber) } else { @@ -503,7 +639,7 @@ func (server *Server) handleGamePlayersPatch(w http.ResponseWriter, r *http.Requ // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.GameEventType, - events.NewGameEventPayload(server.tables[table].Game), + events.NewGameEventPayload(server.tables[tableNum].Game), ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -514,11 +650,28 @@ func (server *Server) handleGamePlayersPatch(w http.ResponseWriter, r *http.Requ server.overlay.Broadcast <- message // send response - server.handleSuccess(w, r, server.tables[table].Game) + server.handleSuccess(w, r, server.tables[tableNum].Game) } // Game players handler for DELETE method. -func (server *Server) handleGamePlayersDelete(w http.ResponseWriter, r *http.Request, table int) { +func (server *Server) handleGamePlayersDelete(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + // decode the body var body GamePlayersDeleteBody decoder := json.NewDecoder(r.Body) @@ -528,7 +681,7 @@ func (server *Server) handleGamePlayersDelete(w http.ResponseWriter, r *http.Req } // unset the current player - if err := server.tables[table].Game.UnsetPlayer(body.PlayerNum); err != nil { + if err := server.tables[tableNum].Game.UnsetPlayer(body.PlayerNum); err != nil { if errors.Is(err, models.ErrInvalidPlayerNumber) { server.handleError(w, r, http.StatusUnprocessableEntity, models.ErrInvalidPlayerNumber) } else { @@ -541,7 +694,7 @@ func (server *Server) handleGamePlayersDelete(w http.ResponseWriter, r *http.Req // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.GameEventType, - events.NewGameEventPayload(server.tables[table].Game), + events.NewGameEventPayload(server.tables[tableNum].Game), ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -552,7 +705,7 @@ func (server *Server) handleGamePlayersDelete(w http.ResponseWriter, r *http.Req server.overlay.Broadcast <- message // send response - server.handleSuccess(w, r, server.tables[table].Game) + server.handleSuccess(w, r, server.tables[tableNum].Game) } // Handler for /game/players/flag @@ -728,13 +881,13 @@ func (server *Server) handleGameTeamsPatch(w http.ResponseWriter, r *http.Reques } // Handler for /game/fargo-hot-handicap. -func (server *Server) handleGameFargoHotHandicap(table int) http.Handler { +func (server *Server) handleGameFargoHotHandicap() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "OPTIONS": server.HandleOptions(w, r) case "PATCH": - server.handleGameFargoHotHandicapPatch(w, r, table) + server.handleGameFargoHotHandicapPatch(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -742,7 +895,24 @@ func (server *Server) handleGameFargoHotHandicap(table int) http.Handler { } // Game fargo-hot-handicap handler for PATCH method. -func (server *Server) handleGameFargoHotHandicapPatch(w http.ResponseWriter, r *http.Request, table int) { +func (server *Server) handleGameFargoHotHandicapPatch(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + // decode the body var body GameFargoHotHandicapPatchBody decoder := json.NewDecoder(r.Body) @@ -752,7 +922,7 @@ func (server *Server) handleGameFargoHotHandicapPatch(w http.ResponseWriter, r * } // Update the game fargo hot handicap option. - if err := server.tables[table].Game.SetUseFargoHotHandicap(body.UseFargoHotHandicap); err != nil { + if err := server.tables[tableNum].Game.SetUseFargoHotHandicap(body.UseFargoHotHandicap); err != nil { // TODO: Currently all errors return as 500 here, but might not always make sense. Could use errors.Is for this. server.handleError(w, r, http.StatusInternalServerError, err) return @@ -761,7 +931,7 @@ func (server *Server) handleGameFargoHotHandicapPatch(w http.ResponseWriter, r * // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.GameEventType, - events.NewGameEventPayload(server.tables[table].Game), + events.NewGameEventPayload(server.tables[tableNum].Game), ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -773,6 +943,6 @@ func (server *Server) handleGameFargoHotHandicapPatch(w http.ResponseWriter, r * // send response server.handleSuccess(w, r, GameFargoHotHandicapPatchResp{ - UseFargoHotHandicap: server.tables[table].Game.UseFargoHotHandicap, + UseFargoHotHandicap: server.tables[tableNum].Game.UseFargoHotHandicap, }) } diff --git a/libs/go/api/overlay.go b/libs/go/api/overlay.go index 3411d64..7f62842 100644 --- a/libs/go/api/overlay.go +++ b/libs/go/api/overlay.go @@ -3,9 +3,11 @@ package api import ( "log" "net/http" + "strconv" "github.com/codephobia/pool-overlay/libs/go/events" "github.com/codephobia/pool-overlay/libs/go/overlay" + "github.com/gorilla/mux" ) // OverlayToggleResp is the response from toggling the overlay. @@ -94,11 +96,11 @@ func (server *Server) handleOverlayGet(w http.ResponseWriter, r *http.Request) { } // Handler for overlay toggle. -func (server *Server) handleOverlayToggle(table int) http.Handler { +func (server *Server) handleOverlayToggle() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - server.handleOverlayToggleGet(w, r, table) + server.handleOverlayToggleGet(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -106,13 +108,30 @@ func (server *Server) handleOverlayToggle(table int) http.Handler { } // Overlay toggle handler for GET method. -func (server *Server) handleOverlayToggleGet(w http.ResponseWriter, r *http.Request, table int) { - hidden := server.tables[table].Overlay.ToggleHidden() +func (server *Server) handleOverlayToggleGet(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + hidden := server.tables[tableNum].Overlay.ToggleHidden() // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.OverlayStateEventType, - server.tables[table].Overlay, + server.tables[tableNum].Overlay, ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -128,11 +147,11 @@ func (server *Server) handleOverlayToggleGet(w http.ResponseWriter, r *http.Requ } // Handler for overlay toggle flags. -func (server *Server) handleOverlayToggleFlags(table int) http.Handler { +func (server *Server) handleOverlayToggleFlags() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - server.handleOverlayToggleFlagsGet(w, r, table) + server.handleOverlayToggleFlagsGet(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -140,13 +159,30 @@ func (server *Server) handleOverlayToggleFlags(table int) http.Handler { } // Overlay toggle flags handler for GET method. -func (server *Server) handleOverlayToggleFlagsGet(w http.ResponseWriter, r *http.Request, table int) { - showFlags := server.tables[table].Overlay.ToggleFlags() +func (server *Server) handleOverlayToggleFlagsGet(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + showFlags := server.tables[tableNum].Overlay.ToggleFlags() // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.OverlayStateEventType, - server.tables[table].Overlay, + server.tables[tableNum].Overlay, ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -162,11 +198,11 @@ func (server *Server) handleOverlayToggleFlagsGet(w http.ResponseWriter, r *http } // Handler for overlay toggle fargo. -func (server *Server) handleOverlayToggleFargo(table int) http.Handler { +func (server *Server) handleOverlayToggleFargo() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - server.handleOverlayToggleFargoGet(w, r, table) + server.handleOverlayToggleFargoGet(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -174,13 +210,30 @@ func (server *Server) handleOverlayToggleFargo(table int) http.Handler { } // Overlay toggle fargo handler for GET method. -func (server *Server) handleOverlayToggleFargoGet(w http.ResponseWriter, r *http.Request, table int) { - showFargo := server.tables[table].Overlay.ToggleFargo() +func (server *Server) handleOverlayToggleFargoGet(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + showFargo := server.tables[tableNum].Overlay.ToggleFargo() // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.OverlayStateEventType, - server.tables[table].Overlay, + server.tables[tableNum].Overlay, ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) @@ -196,11 +249,11 @@ func (server *Server) handleOverlayToggleFargoGet(w http.ResponseWriter, r *http } // Handler for overlay toggle score. -func (server *Server) handleOverlayToggleScore(table int) http.Handler { +func (server *Server) handleOverlayToggleScore() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - server.handleOverlayToggleScoreGet(w, r, table) + server.handleOverlayToggleScoreGet(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -208,13 +261,30 @@ func (server *Server) handleOverlayToggleScore(table int) http.Handler { } // Overlay toggle score handler for GET method. -func (server *Server) handleOverlayToggleScoreGet(w http.ResponseWriter, r *http.Request, table int) { - showScore := server.tables[table].Overlay.ToggleScore() +func (server *Server) handleOverlayToggleScoreGet(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url + params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + showScore := server.tables[tableNum].Overlay.ToggleScore() // Generate message to broadcast to overlay. message, err := overlay.NewEvent( events.OverlayStateEventType, - server.tables[table].Overlay, + server.tables[tableNum].Overlay, ).ToBytes() if err != nil { server.handleError(w, r, http.StatusUnprocessableEntity, ErrUnableToBroadcastUpdate) diff --git a/libs/go/api/routes.go b/libs/go/api/routes.go index 83e958d..327a5b8 100644 --- a/libs/go/api/routes.go +++ b/libs/go/api/routes.go @@ -30,28 +30,16 @@ func (server *Server) InitRoutes() { server.AddRouteToAllVersions("/overlay", server.handleOverlay()) // overlay/toggle - // server.AddRouteToAllVersions("/overlay/toggle", server.handleOverlayToggle()) - server.AddRouteToAllVersions("/table/1/overlay/toggle", server.handleOverlayToggle(1)) - server.AddRouteToAllVersions("/table/2/overlay/toggle", server.handleOverlayToggle(2)) - server.AddRouteToAllVersions("/table/3/overlay/toggle", server.handleOverlayToggle(3)) + server.AddRouteToAllVersions("/table/{tableNum}/overlay/toggle", server.handleOverlayToggle()) // overlay/toggle/flags - // server.AddRouteToAllVersions("/overlay/toggle/flags", server.handleOverlayToggleFlags()) - server.AddRouteToAllVersions("/table/1/overlay/toggle/flags", server.handleOverlayToggleFlags(1)) - server.AddRouteToAllVersions("/table/2/overlay/toggle/flags", server.handleOverlayToggleFlags(2)) - server.AddRouteToAllVersions("/table/3/overlay/toggle/flags", server.handleOverlayToggleFlags(3)) + server.AddRouteToAllVersions("/table/{tableNum}/overlay/toggle/flags", server.handleOverlayToggleFlags()) // overlay/toggle/fargo - // server.AddRouteToAllVersions("/overlay/toggle/fargo", server.handleOverlayToggleFargo()) - server.AddRouteToAllVersions("/table/1/overlay/toggle/fargo", server.handleOverlayToggleFargo(1)) - server.AddRouteToAllVersions("/table/2/overlay/toggle/fargo", server.handleOverlayToggleFargo(2)) - server.AddRouteToAllVersions("/table/3/overlay/toggle/fargo", server.handleOverlayToggleFargo(3)) + server.AddRouteToAllVersions("/table/{tableNum}/overlay/toggle/fargo", server.handleOverlayToggleFargo()) // overlay/toggle/score - // server.AddRouteToAllVersions("/overlay/toggle/score", server.handleOverlayToggleScore()) - server.AddRouteToAllVersions("/table/1/overlay/toggle/score", server.handleOverlayToggleScore(1)) - server.AddRouteToAllVersions("/table/2/overlay/toggle/score", server.handleOverlayToggleScore(2)) - server.AddRouteToAllVersions("/table/3/overlay/toggle/score", server.handleOverlayToggleScore(3)) + server.AddRouteToAllVersions("/table/{tableNum}/overlay/toggle/score", server.handleOverlayToggleScore()) // web socket connection to telestrator server.AddRouteToAllVersions("/telestrator", server.handleTelestrator()) @@ -74,40 +62,35 @@ func (server *Server) InitRoutes() { // flags server.AddRouteToAllVersions("/flags", server.handleFlags()) + // table/count + server.AddRouteToAllVersions("/table/count", server.handleTableCount()) + + // table/add + server.AddRouteToAllVersions("/table/add", server.handleTableAdd()) + + // table/remove + server.AddRouteToAllVersions("/table/remove", server.handleTableRemove()) + // table/swap - server.AddRouteToAllVersions("/table/1/swap/{newTable}", server.handleTableSwap(1)) - server.AddRouteToAllVersions("/table/2/swap/{newTable}", server.handleTableSwap(2)) - server.AddRouteToAllVersions("/table/3/swap/{newTable}", server.handleTableSwap(3)) + server.AddRouteToAllVersions("/table/{tableNum}/swap/{newTable}", server.handleTableSwap()) // game - server.AddRouteToAllVersions("/table/1/game", server.handleGame(1)) - server.AddRouteToAllVersions("/table/2/game", server.handleGame(2)) - server.AddRouteToAllVersions("/table/3/game", server.handleGame(3)) + server.AddRouteToAllVersions("/table/{tableNum}/game", server.handleGame()) // game/type - server.AddRouteToAllVersions("/table/1/game/type", server.handleGameType(1)) - server.AddRouteToAllVersions("/table/2/game/type", server.handleGameType(2)) - server.AddRouteToAllVersions("/table/3/game/type", server.handleGameType(3)) + server.AddRouteToAllVersions("/table/{tableNum}/game/type", server.handleGameType()) // game/vs-mode - server.AddRouteToAllVersions("/table/1/game/vs-mode", server.handleGameVsMode(1)) - server.AddRouteToAllVersions("/table/2/game/vs-mode", server.handleGameVsMode(2)) - server.AddRouteToAllVersions("/table/3/game/vs-mode", server.handleGameVsMode(3)) + server.AddRouteToAllVersions("/table/{tableNum}/game/vs-mode", server.handleGameVsMode()) // game/race-to - server.AddRouteToAllVersions("/table/1/game/race-to", server.handleGameRaceTo(1)) - server.AddRouteToAllVersions("/table/2/game/race-to", server.handleGameRaceTo(2)) - server.AddRouteToAllVersions("/table/3/game/race-to", server.handleGameRaceTo(3)) + server.AddRouteToAllVersions("/table/{tableNum}/game/race-to", server.handleGameRaceTo()) // game/score - server.AddRouteToAllVersions("/table/1/game/score", server.handleGameScore(1)) - server.AddRouteToAllVersions("/table/2/game/score", server.handleGameScore(2)) - server.AddRouteToAllVersions("/table/3/game/score", server.handleGameScore(3)) + server.AddRouteToAllVersions("/table/{tableNum}/game/score", server.handleGameScore()) // game/players - server.AddRouteToAllVersions("/table/1/game/players", server.handleGamePlayers(1)) - server.AddRouteToAllVersions("/table/2/game/players", server.handleGamePlayers(2)) - server.AddRouteToAllVersions("/table/3/game/players", server.handleGamePlayers(3)) + server.AddRouteToAllVersions("/table/{tableNum}/game/players", server.handleGamePlayers()) // game/players/flag server.AddRouteToAllVersions("/game/players/flag", server.handleGamePlayersFlag()) @@ -119,9 +102,7 @@ func (server *Server) InitRoutes() { server.AddRouteToAllVersions("/game/teams", server.handleGameTeams()) // game/fargo-hot-handicap - server.AddRouteToAllVersions("/table/1/game/fargo-hot-handicap", server.handleGameFargoHotHandicap(1)) - server.AddRouteToAllVersions("/table/2/game/fargo-hot-handicap", server.handleGameFargoHotHandicap(2)) - server.AddRouteToAllVersions("/table/3/game/fargo-hot-handicap", server.handleGameFargoHotHandicap(3)) + server.AddRouteToAllVersions("/table/{tableNum}/game/fargo-hot-handicap", server.handleGameFargoHotHandicap()) // tournament server.AddRouteToAllVersions("/tournament", server.handleTournament()) diff --git a/libs/go/api/tables.go b/libs/go/api/tables.go index 27fb841..2684944 100644 --- a/libs/go/api/tables.go +++ b/libs/go/api/tables.go @@ -6,17 +6,95 @@ import ( "github.com/codephobia/pool-overlay/libs/go/events" "github.com/codephobia/pool-overlay/libs/go/overlay" + "github.com/codephobia/pool-overlay/libs/go/state" "github.com/gorilla/mux" ) +// Handler for /table/count. +func (server *Server) handleTableCount() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "OPTIONS": + server.HandleOptions(w, r) + case "GET": + server.handleTableCountGet(w, r) + default: + server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) + } + }) +} + +// Table count handler for GET method. +func (server *Server) handleTableCountGet(w http.ResponseWriter, r *http.Request) { + count := int64(len(server.tables)) + server.handleSuccess(w, r, &CountResp{ + Count: count, + }) +} + +// Handler for /table/add. +func (server *Server) handleTableAdd() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "OPTIONS": + server.HandleOptions(w, r) + case "POST": + server.handleTableAddPost(w, r) + default: + server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) + } + }) +} + +// Table add handler for POST method. +func (server *Server) handleTableAddPost(w http.ResponseWriter, r *http.Request) { + count := len(server.tables) + server.tables[count+1] = state.NewState(server.db, count+1) + + server.handleSuccess(w, r, &CountResp{ + Count: int64(len(server.tables)), + }) +} + +// Handler for /table/remove. +func (server *Server) handleTableRemove() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "OPTIONS": + server.HandleOptions(w, r) + case "POST": + server.handleTableRemovePost(w, r) + default: + server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) + } + }) +} + +// Table remove handler for POST method. +func (server *Server) handleTableRemovePost(w http.ResponseWriter, r *http.Request) { + count := len(server.tables) + + // don't allow removal of single table + if count == 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrRemoveOnlyTable) + return + } + + delete(server.tables, count) + + server.handleSuccess(w, r, &CountResp{ + Count: int64(len(server.tables)), + }) +} + // Handler for /table/swap. -func (server *Server) handleTableSwap(table int) http.Handler { +func (server *Server) handleTableSwap() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "OPTIONS": server.HandleOptions(w, r) case "GET": - server.handleTableSwapGet(w, r, table) + server.handleTableSwapGet(w, r) default: server.handleError(w, r, http.StatusMethodNotAllowed, ErrEndpointMethodNotAllowed) } @@ -24,9 +102,25 @@ func (server *Server) handleTableSwap(table int) http.Handler { } // Table swap handler for GET method. -func (server *Server) handleTableSwapGet(w http.ResponseWriter, r *http.Request, table int) { - // get param for new table num from url +func (server *Server) handleTableSwapGet(w http.ResponseWriter, r *http.Request) { + // get params for table numbers from url params := mux.Vars(r) + + // get table number + tableNumValue, ok := params["tableNum"] + if !ok || len(tableNumValue) < 1 { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // convert table number to int + tableNum, err := strconv.Atoi(tableNumValue) + if err != nil || tableNum > len(server.tables) { + server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) + return + } + + // get new table number newTableValue, ok := params["newTable"] if !ok || len(newTableValue) < 1 { server.handleError(w, r, http.StatusUnprocessableEntity, ErrInvalidTableNumber) @@ -41,23 +135,23 @@ func (server *Server) handleTableSwapGet(w http.ResponseWriter, r *http.Request, } // swap table states - server.tables[table].Table = newTable - server.tables[table].Game.Table = newTable - server.tables[table].Overlay.Table = newTable + server.tables[tableNum].Table = newTable + server.tables[tableNum].Game.Table = newTable + server.tables[tableNum].Overlay.Table = newTable - server.tables[newTable].Table = table - server.tables[newTable].Game.Table = table - server.tables[newTable].Overlay.Table = table + server.tables[newTable].Table = tableNum + server.tables[newTable].Game.Table = tableNum + server.tables[newTable].Overlay.Table = tableNum - server.tables[table], server.tables[newTable] = server.tables[newTable], server.tables[table] + server.tables[tableNum], server.tables[newTable] = server.tables[newTable], server.tables[tableNum] // update current matches in tournament mode if running if server.challonge.InTournamentMode() { - server.challonge.CurrentMatches[table], server.challonge.CurrentMatches[newTable] = server.challonge.CurrentMatches[newTable], server.challonge.CurrentMatches[table] + server.challonge.CurrentMatches[tableNum], server.challonge.CurrentMatches[newTable] = server.challonge.CurrentMatches[newTable], server.challonge.CurrentMatches[tableNum] } // broadcast out changes for each table - changedTables := []int{table, newTable} + changedTables := []int{tableNum, newTable} for i := 0; i < len(changedTables); i++ { // Generate current game state message. diff --git a/libs/go/apidocs/overlay-toggle_swagger.go b/libs/go/apidocs/overlay-toggle_swagger.go index 723a457..ecac796 100644 --- a/libs/go/apidocs/overlay-toggle_swagger.go +++ b/libs/go/apidocs/overlay-toggle_swagger.go @@ -2,13 +2,23 @@ package apidocs import "github.com/codephobia/pool-overlay/libs/go/api" -// swagger:route GET /overlay/toggle overlay OverlayToggle +// swagger:route GET /table/{tableNum}/overlay/toggle overlay OverlayToggle // Toggles showing / hiding of the overlay. // responses: // 200: overlayToggleResp // 422: errorResp -//nolint +// swagger:parameters OverlayToggle +type OverlayToggleGetParam struct { + // The table number to toggle the overlay on. + // + // in: path + // required: true + // example: 1 + TableNum int `json:"tableNum"` +} + +// nolint // Returns updated value for Hidden. // swagger:response overlayToggleResp type overlayToggleRespWrapper struct { diff --git a/libs/go/apidocs/table-count_swagger.go b/libs/go/apidocs/table-count_swagger.go new file mode 100644 index 0000000..a84594a --- /dev/null +++ b/libs/go/apidocs/table-count_swagger.go @@ -0,0 +1,17 @@ +package apidocs + +import ( + "github.com/codephobia/pool-overlay/libs/go/api" +) + +// swagger:route GET /table/count table TableCount +// The number of current tables in use. +// responses: +// 200: tableCountResp + +// The number of tables in use. +// swagger:response tableCountResp +type TableCountRespWrapper struct { + // in: body + Body api.CountResp +} diff --git a/libs/go/apidocs/table_swagger.go b/libs/go/apidocs/table_swagger.go new file mode 100644 index 0000000..2ec7477 --- /dev/null +++ b/libs/go/apidocs/table_swagger.go @@ -0,0 +1,30 @@ +package apidocs + +import ( + "github.com/codephobia/pool-overlay/libs/go/api" +) + +// swagger:route POST /table/add table TableAdd +// Add a table to the current count. +// responses: +// 200: tableAddCountResp + +// The number of tables in use. +// swagger:response tableAddCountResp +type TableAddRespWrapper struct { + // in: body + Body api.CountResp +} + +// swagger:route POST /table/remove table TableRemove +// Removes a table from the current count. +// responses: +// 200: tableRemoveCountResp +// 422: errorResp + +// The number of tables in use. +// swagger:response tableRemoveCountResp +type TableRemoveRespWrapper struct { + // in: body + Body api.CountResp +} diff --git a/package-lock.json b/package-lock.json index d2d85ef..b5d6530 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@nativescript/angular": "^15.0.0", "@nativescript/core": "~8.4.0", "@ngrx/component-store": "15.0.0", + "@ngrx/effects": "^15.0.0", "@ngrx/router-store": "15.0.0", "@ngrx/store": "^15.0.0", "@ngrx/store-devtools": "^15.0.0", @@ -5518,6 +5519,19 @@ "rxjs": "^6.5.3 || ^7.5.0" } }, + "node_modules/@ngrx/effects": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-15.0.0.tgz", + "integrity": "sha512-EfYmGYFF1bNLPCRPnfAZnppT47RPwpprZK6HSQQ1fvV0sA0FYKQilg93ictNhDv+0IhMBZJEbR/hBMYyi4rkBg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^15.0.0", + "@ngrx/store": "15.0.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, "node_modules/@ngrx/router-store": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-15.0.0.tgz", @@ -28622,6 +28636,14 @@ "tslib": "^2.0.0" } }, + "@ngrx/effects": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-15.0.0.tgz", + "integrity": "sha512-EfYmGYFF1bNLPCRPnfAZnppT47RPwpprZK6HSQQ1fvV0sA0FYKQilg93ictNhDv+0IhMBZJEbR/hBMYyi4rkBg==", + "requires": { + "tslib": "^2.0.0" + } + }, "@ngrx/router-store": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-15.0.0.tgz", diff --git a/package.json b/package.json index 8fa75a6..842864e 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@nativescript/angular": "^15.0.0", "@nativescript/core": "~8.4.0", "@ngrx/component-store": "15.0.0", + "@ngrx/effects": "^15.0.0", "@ngrx/router-store": "15.0.0", "@ngrx/store": "^15.0.0", "@ngrx/store-devtools": "^15.0.0",