updated slapapi
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ internal/view/**/*_templ.go
|
|||||||
internal/view/**/*_templ.txt
|
internal/view/**/*_templ.txt
|
||||||
cmd/test/*
|
cmd/test/*
|
||||||
.opencode
|
.opencode
|
||||||
|
Matches/
|
||||||
|
|
||||||
# Database backups (compressed)
|
# Database backups (compressed)
|
||||||
backups/*.sql.gz
|
backups/*.sql.gz
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package slapshotapi
|
package slapshotapi
|
||||||
|
|
||||||
|
// Regions
|
||||||
const (
|
const (
|
||||||
RegionEUWest = "eu-west"
|
RegionEUWest = "eu-west"
|
||||||
RegionNAEast = "na-east"
|
RegionNAEast = "na-east"
|
||||||
RegionNACentral = "na-central"
|
RegionNACentral = "na-central"
|
||||||
RegionNAWest = "na-west"
|
RegionNAWest = "na-west"
|
||||||
RegionOCEEast = "oce-east"
|
RegionOCEEast = "oce-east"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Arenas - API format (used in API responses and lobby creation)
|
||||||
|
const (
|
||||||
ArenaSlapstadium = "Slapstadium"
|
ArenaSlapstadium = "Slapstadium"
|
||||||
ArenaSlapville = "Slapville"
|
ArenaSlapville = "Slapville"
|
||||||
ArenaSlapstadiumMini = "Slapstadium_mini"
|
ArenaSlapstadiumMini = "Slapstadium_mini"
|
||||||
@@ -18,7 +22,25 @@ const (
|
|||||||
ArenaIsland = "Island"
|
ArenaIsland = "Island"
|
||||||
ArenaObstacles = "Obstacles"
|
ArenaObstacles = "Obstacles"
|
||||||
ArenaObstaclesXL = "Obstacles_XL"
|
ArenaObstaclesXL = "Obstacles_XL"
|
||||||
|
ArenaCyberpuck = "Cyberpuck"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Arenas - Display format (used in local match logs)
|
||||||
|
const (
|
||||||
|
ArenaDisplaySlapStadium = "Slap Stadium"
|
||||||
|
ArenaDisplaySlapville = "Slapville"
|
||||||
|
ArenaDisplaySlapStadiumMini = "Slap Stadium Mini"
|
||||||
|
ArenaDisplayTableHockey = "Table Hockey"
|
||||||
|
ArenaDisplayColosseum = "Colosseum"
|
||||||
|
ArenaDisplaySlapvilleJumbo = "Slapville Jumbo"
|
||||||
|
ArenaDisplaySlapstadiumXL = "Slapstadium XL"
|
||||||
|
ArenaDisplayIsland = "Island"
|
||||||
|
ArenaDisplayObstaclesXL = "Obstacles XL"
|
||||||
|
ArenaDisplayCyberpuck = "Cyberpuck"
|
||||||
|
)
|
||||||
|
|
||||||
|
// End reasons
|
||||||
|
const (
|
||||||
EndReasonEndOfReg = "EndOfRegulation"
|
EndReasonEndOfReg = "EndOfRegulation"
|
||||||
EndReasonOvertime = "Overtime"
|
EndReasonOvertime = "Overtime"
|
||||||
EndReasonHomeTeamLeft = "HomeTeamLeft"
|
EndReasonHomeTeamLeft = "HomeTeamLeft"
|
||||||
@@ -28,8 +50,30 @@ const (
|
|||||||
EndReasonForfeit = "Forfeit"
|
EndReasonForfeit = "Forfeit"
|
||||||
EndReasonCancelled = "Cancelled"
|
EndReasonCancelled = "Cancelled"
|
||||||
EndReasonUnknown = "Unknown"
|
EndReasonUnknown = "Unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Game modes
|
||||||
|
const (
|
||||||
GameModeHockey = "hockey"
|
GameModeHockey = "hockey"
|
||||||
GameModeDodgePuck = "dodgepuck"
|
GameModeDodgePuck = "dodgepuck"
|
||||||
GameModeTag = "tag"
|
GameModeTag = "tag"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Teams
|
||||||
|
const (
|
||||||
|
TeamHome = "home"
|
||||||
|
TeamAway = "away"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Match winners
|
||||||
|
const (
|
||||||
|
WinnerHome = "home"
|
||||||
|
WinnerAway = "away"
|
||||||
|
WinnerNone = "none"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Match types
|
||||||
|
const (
|
||||||
|
MatchTypePublic = "public"
|
||||||
|
MatchTypePrivate = "private"
|
||||||
|
)
|
||||||
|
|||||||
53
pkg/slapshotapi/games.go
Normal file
53
pkg/slapshotapi/games.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package slapshotapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type endpointGame struct {
|
||||||
|
gameID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEndpointGame(gameID string) *endpointGame {
|
||||||
|
return &endpointGame{
|
||||||
|
gameID: gameID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointGame) path() string {
|
||||||
|
return fmt.Sprintf("/api/public/games/%s", ep.gameID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointGame) method() string {
|
||||||
|
return "GET"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrGameNotFound is returned when a game ID does not match any known game
|
||||||
|
var ErrGameNotFound = errors.New("game not found")
|
||||||
|
|
||||||
|
// GetGame retrieves match details for a specific game by its ID.
|
||||||
|
func (c *SlapAPI) GetGame(
|
||||||
|
ctx context.Context,
|
||||||
|
gameID string,
|
||||||
|
) (*Game, error) {
|
||||||
|
if gameID == "" {
|
||||||
|
return nil, errors.New("gameID cannot be empty")
|
||||||
|
}
|
||||||
|
endpoint := getEndpointGame(gameID)
|
||||||
|
data, err := c.request(ctx, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "404") {
|
||||||
|
return nil, ErrGameNotFound
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "c.request")
|
||||||
|
}
|
||||||
|
game, err := unmarshal[Game](data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "unmarshal")
|
||||||
|
}
|
||||||
|
return game, nil
|
||||||
|
}
|
||||||
187
pkg/slapshotapi/lobbies.go
Normal file
187
pkg/slapshotapi/lobbies.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package slapshotapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Get Lobby ---
|
||||||
|
|
||||||
|
type endpointGetLobby struct {
|
||||||
|
lobbyID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEndpointGetLobby(lobbyID string) *endpointGetLobby {
|
||||||
|
return &endpointGetLobby{lobbyID: lobbyID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointGetLobby) path() string {
|
||||||
|
return fmt.Sprintf("/api/public/lobbies/%s", ep.lobbyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointGetLobby) method() string {
|
||||||
|
return "GET"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrLobbyNotFound is returned when a lobby ID does not match any known lobby
|
||||||
|
var ErrLobbyNotFound = errors.New("lobby not found")
|
||||||
|
|
||||||
|
// GetLobby retrieves details for a specific lobby by its ID.
|
||||||
|
func (c *SlapAPI) GetLobby(
|
||||||
|
ctx context.Context,
|
||||||
|
lobbyID string,
|
||||||
|
) (*Lobby, error) {
|
||||||
|
if lobbyID == "" {
|
||||||
|
return nil, errors.New("lobbyID cannot be empty")
|
||||||
|
}
|
||||||
|
endpoint := getEndpointGetLobby(lobbyID)
|
||||||
|
data, err := c.request(ctx, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "404") {
|
||||||
|
return nil, ErrLobbyNotFound
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "c.request")
|
||||||
|
}
|
||||||
|
lobby, err := unmarshal[Lobby](data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "unmarshal")
|
||||||
|
}
|
||||||
|
return lobby, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Get Lobby Matches ---
|
||||||
|
|
||||||
|
type endpointGetLobbyMatches struct {
|
||||||
|
lobbyID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEndpointGetLobbyMatches(lobbyID string) *endpointGetLobbyMatches {
|
||||||
|
return &endpointGetLobbyMatches{lobbyID: lobbyID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointGetLobbyMatches) path() string {
|
||||||
|
return fmt.Sprintf("/api/public/lobbies/%s/matches", ep.lobbyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointGetLobbyMatches) method() string {
|
||||||
|
return "GET"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLobbyMatches retrieves the list of matches played in a specific lobby.
|
||||||
|
func (c *SlapAPI) GetLobbyMatches(
|
||||||
|
ctx context.Context,
|
||||||
|
lobbyID string,
|
||||||
|
) ([]Game, error) {
|
||||||
|
if lobbyID == "" {
|
||||||
|
return nil, errors.New("lobbyID cannot be empty")
|
||||||
|
}
|
||||||
|
endpoint := getEndpointGetLobbyMatches(lobbyID)
|
||||||
|
data, err := c.request(ctx, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "404") {
|
||||||
|
return nil, ErrLobbyNotFound
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "c.request")
|
||||||
|
}
|
||||||
|
var games []Game
|
||||||
|
err = json.Unmarshal(data, &games)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "json.Unmarshal")
|
||||||
|
}
|
||||||
|
return games, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Create Lobby ---
|
||||||
|
|
||||||
|
type endpointCreateLobby struct {
|
||||||
|
request LobbyCreationRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEndpointCreateLobby(req LobbyCreationRequest) *endpointCreateLobby {
|
||||||
|
return &endpointCreateLobby{request: req}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointCreateLobby) path() string {
|
||||||
|
return "/api/public/lobbies"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointCreateLobby) method() string {
|
||||||
|
return "POST"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointCreateLobby) body() ([]byte, error) {
|
||||||
|
data, err := json.Marshal(ep.request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "json.Marshal")
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLobby creates a new custom lobby with the specified settings.
|
||||||
|
func (c *SlapAPI) CreateLobby(
|
||||||
|
ctx context.Context,
|
||||||
|
req LobbyCreationRequest,
|
||||||
|
) (*LobbyCreationResponse, error) {
|
||||||
|
if req.Region == "" {
|
||||||
|
return nil, errors.New("region cannot be empty")
|
||||||
|
}
|
||||||
|
if req.Name == "" {
|
||||||
|
return nil, errors.New("name cannot be empty")
|
||||||
|
}
|
||||||
|
if req.CreatorName == "" {
|
||||||
|
return nil, errors.New("creator_name cannot be empty")
|
||||||
|
}
|
||||||
|
endpoint := getEndpointCreateLobby(req)
|
||||||
|
data, err := c.request(ctx, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "c.request")
|
||||||
|
}
|
||||||
|
resp, err := unmarshal[LobbyCreationResponse](data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "unmarshal")
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Delete Lobby ---
|
||||||
|
|
||||||
|
type endpointDeleteLobby struct {
|
||||||
|
lobbyID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEndpointDeleteLobby(lobbyID string) *endpointDeleteLobby {
|
||||||
|
return &endpointDeleteLobby{lobbyID: lobbyID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointDeleteLobby) path() string {
|
||||||
|
return fmt.Sprintf("/api/public/lobbies/%s", ep.lobbyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *endpointDeleteLobby) method() string {
|
||||||
|
return "DELETE"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteLobby deletes an existing lobby by its ID.
|
||||||
|
// Returns true if the lobby was successfully deleted.
|
||||||
|
func (c *SlapAPI) DeleteLobby(
|
||||||
|
ctx context.Context,
|
||||||
|
lobbyID string,
|
||||||
|
) (bool, error) {
|
||||||
|
if lobbyID == "" {
|
||||||
|
return false, errors.New("lobbyID cannot be empty")
|
||||||
|
}
|
||||||
|
endpoint := getEndpointDeleteLobby(lobbyID)
|
||||||
|
status, body, err := c.requestRaw(ctx, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "c.requestRaw")
|
||||||
|
}
|
||||||
|
if status == http.StatusNotFound {
|
||||||
|
return false, ErrLobbyNotFound
|
||||||
|
}
|
||||||
|
return string(body) == "OK", nil
|
||||||
|
}
|
||||||
64
pkg/slapshotapi/matchlog.go
Normal file
64
pkg/slapshotapi/matchlog.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package slapshotapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MatchLog represents the raw JSON format of a local match log file.
|
||||||
|
// This differs slightly from the API Game response in structure — the
|
||||||
|
// match log is a flat object with game stats at the top level, whereas
|
||||||
|
// the API response wraps game stats inside a nested "game_stats" field
|
||||||
|
// alongside metadata like region, match type, and creation time.
|
||||||
|
type MatchLog struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
MatchID string `json:"match_id,omitempty"`
|
||||||
|
Winner string `json:"winner"`
|
||||||
|
Arena string `json:"arena"`
|
||||||
|
PeriodsEnabled string `json:"periods_enabled"`
|
||||||
|
CurrentPeriod string `json:"current_period"`
|
||||||
|
CustomMercyRule string `json:"custom_mercy_rule"`
|
||||||
|
MatchLength string `json:"match_length"`
|
||||||
|
EndReason string `json:"end_reason"`
|
||||||
|
Score Score `json:"score"`
|
||||||
|
Players []Player `json:"players"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMatchLog parses a local match log JSON file into a MatchLog struct.
|
||||||
|
func ParseMatchLog(data []byte) (*MatchLog, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, errors.New("data cannot be empty")
|
||||||
|
}
|
||||||
|
log, err := unmarshal[MatchLog](data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "unmarshal")
|
||||||
|
}
|
||||||
|
return log, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToGameStats converts a MatchLog into a GameStats struct, normalizing the
|
||||||
|
// local match log format into the same structure used by the API.
|
||||||
|
func (ml *MatchLog) ToGameStats() GameStats {
|
||||||
|
return GameStats{
|
||||||
|
Type: ml.Type,
|
||||||
|
Arena: ml.Arena,
|
||||||
|
Score: ml.Score,
|
||||||
|
Winner: ml.Winner,
|
||||||
|
EndReason: ml.EndReason,
|
||||||
|
MatchLength: ml.MatchLength,
|
||||||
|
PeriodsEnabled: ml.PeriodsEnabled,
|
||||||
|
CurrentPeriod: ml.CurrentPeriod,
|
||||||
|
CustomMercyRule: ml.CustomMercyRule,
|
||||||
|
Players: ml.Players,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToGame converts a MatchLog into a Game struct. Since match logs don't
|
||||||
|
// contain all fields present in an API response (region, match_type,
|
||||||
|
// gamemode, created), those fields will be empty. The match_id from the
|
||||||
|
// log (if present) is used as the Game ID.
|
||||||
|
func (ml *MatchLog) ToGame() Game {
|
||||||
|
return Game{
|
||||||
|
ID: ml.MatchID,
|
||||||
|
GameStats: ml.ToGameStats(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package slapshotapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -54,9 +53,11 @@ func (c *SlapAPI) GetQueueStatus(
|
|||||||
endpoint := getEndpointMatchmaking(regions)
|
endpoint := getEndpointMatchmaking(regions)
|
||||||
data, err := c.request(ctx, endpoint)
|
data, err := c.request(ctx, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "slapapiReq")
|
return nil, errors.Wrap(err, "c.request")
|
||||||
|
}
|
||||||
|
resp, err := unmarshal[matchmakingresp](data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "unmarshal")
|
||||||
}
|
}
|
||||||
resp := matchmakingresp{}
|
|
||||||
json.Unmarshal(data, &resp)
|
|
||||||
return &resp.Playlists, nil
|
return &resp.Playlists, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package slapshotapi
|
package slapshotapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -14,31 +16,101 @@ type endpoint interface {
|
|||||||
method() string
|
method() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bodyEndpoint is an optional interface for endpoints that send a JSON body
|
||||||
|
type bodyEndpoint interface {
|
||||||
|
endpoint
|
||||||
|
body() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *SlapAPI) request(
|
func (c *SlapAPI) request(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
ep endpoint,
|
ep endpoint,
|
||||||
) ([]byte, error) {
|
) ([]byte, error) {
|
||||||
baseurl := fmt.Sprintf("https://%s.slapshot.gg%s", c.env, ep.path())
|
baseurl := fmt.Sprintf("https://%s.slapshot.gg%s", c.env, ep.path())
|
||||||
req, err := http.NewRequest(ep.method(), baseurl, nil)
|
|
||||||
if err != nil {
|
var bodyReader io.Reader
|
||||||
return nil, errors.Wrap(err, "http.NewRequest")
|
if bep, ok := ep.(bodyEndpoint); ok {
|
||||||
|
data, err := bep.body()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "endpoint.body")
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewReader(data)
|
||||||
}
|
}
|
||||||
req.Header.Add("accept", "application/json")
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, ep.method(), baseurl, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "http.NewRequestWithContext")
|
||||||
|
}
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.key))
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.key))
|
||||||
|
if bodyReader != nil {
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
res, err := c.do(ctx, req)
|
res, err := c.do(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "http.DefaultClient.Do")
|
return nil, errors.Wrap(err, "c.do")
|
||||||
}
|
}
|
||||||
if res.StatusCode != 200 {
|
defer func() { _ = res.Body.Close() }()
|
||||||
return nil, errors.New(fmt.Sprintf("Error making request: %v", res.StatusCode))
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, errors.New(fmt.Sprintf("API request failed with status %d", res.StatusCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "io.ReadAll")
|
return nil, errors.Wrap(err, "io.ReadAll")
|
||||||
}
|
}
|
||||||
err = res.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "resp.Body.Close")
|
|
||||||
}
|
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requestRaw performs an API request and returns the raw status code and body.
|
||||||
|
// This is used for endpoints where non-200 status codes carry meaningful data
|
||||||
|
// (e.g. DELETE returning a plain text response).
|
||||||
|
func (c *SlapAPI) requestRaw(
|
||||||
|
ctx context.Context,
|
||||||
|
ep endpoint,
|
||||||
|
) (int, []byte, error) {
|
||||||
|
baseurl := fmt.Sprintf("https://%s.slapshot.gg%s", c.env, ep.path())
|
||||||
|
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if bep, ok := ep.(bodyEndpoint); ok {
|
||||||
|
data, err := bep.body()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, errors.Wrap(err, "endpoint.body")
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, ep.method(), baseurl, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, errors.Wrap(err, "http.NewRequestWithContext")
|
||||||
|
}
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.key))
|
||||||
|
if bodyReader != nil {
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.do(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, errors.Wrap(err, "c.do")
|
||||||
|
}
|
||||||
|
defer func() { _ = res.Body.Close() }()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, errors.Wrap(err, "io.ReadAll")
|
||||||
|
}
|
||||||
|
return res.StatusCode, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal is a helper that unmarshals JSON response data into a target struct
|
||||||
|
func unmarshal[T any](data []byte) (*T, error) {
|
||||||
|
var result T
|
||||||
|
err := json.Unmarshal(data, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "json.Unmarshal")
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package slapshotapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -46,10 +45,9 @@ func (c *SlapAPI) GetSlapID(
|
|||||||
}
|
}
|
||||||
return 0, errors.Wrap(err, "c.request")
|
return 0, errors.Wrap(err, "c.request")
|
||||||
}
|
}
|
||||||
resp := idresp{}
|
resp, err := unmarshal[idresp](data)
|
||||||
err = json.Unmarshal(data, &resp)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errors.Wrap(err, "json.Unmarshal")
|
return 0, errors.Wrap(err, "unmarshal")
|
||||||
}
|
}
|
||||||
return resp.ID, nil
|
return resp.ID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
158
pkg/slapshotapi/types.go
Normal file
158
pkg/slapshotapi/types.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package slapshotapi
|
||||||
|
|
||||||
|
// Score represents the score for both teams in a match
|
||||||
|
type Score struct {
|
||||||
|
Home int `json:"home"`
|
||||||
|
Away int `json:"away"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlayerStats contains all possible player statistics from a match.
|
||||||
|
// Fields use pointers because not every stat is present in every match response.
|
||||||
|
type PlayerStats struct {
|
||||||
|
Goals *float64 `json:"goals,omitempty"`
|
||||||
|
Assists *float64 `json:"assists,omitempty"`
|
||||||
|
PrimaryAssists *float64 `json:"primary_assists,omitempty"`
|
||||||
|
SecondaryAssists *float64 `json:"secondary_assists,omitempty"`
|
||||||
|
Saves *float64 `json:"saves,omitempty"`
|
||||||
|
Blocks *float64 `json:"blocks,omitempty"`
|
||||||
|
Shots *float64 `json:"shots,omitempty"`
|
||||||
|
Turnovers *float64 `json:"turnovers,omitempty"`
|
||||||
|
Takeaways *float64 `json:"takeaways,omitempty"`
|
||||||
|
Passes *float64 `json:"passes,omitempty"`
|
||||||
|
PossessionTime *float64 `json:"possession_time_sec,omitempty"`
|
||||||
|
FaceoffsWon *float64 `json:"faceoffs_won,omitempty"`
|
||||||
|
FaceoffsLost *float64 `json:"faceoffs_lost,omitempty"`
|
||||||
|
PostHits *float64 `json:"post_hits,omitempty"`
|
||||||
|
OvertimeGoals *float64 `json:"overtime_goals,omitempty"`
|
||||||
|
GameWinningGoals *float64 `json:"game_winning_goals,omitempty"`
|
||||||
|
Score *float64 `json:"score,omitempty"`
|
||||||
|
ContributedGoals *float64 `json:"contributed_goals,omitempty"`
|
||||||
|
ConcededGoals *float64 `json:"conceded_goals,omitempty"`
|
||||||
|
GamesPlayed *float64 `json:"games_played,omitempty"`
|
||||||
|
Wins *float64 `json:"wins,omitempty"`
|
||||||
|
Losses *float64 `json:"losses,omitempty"`
|
||||||
|
OvertimeWins *float64 `json:"overtime_wins,omitempty"`
|
||||||
|
OvertimeLosses *float64 `json:"overtime_losses,omitempty"`
|
||||||
|
Ties *float64 `json:"ties,omitempty"`
|
||||||
|
Shutouts *float64 `json:"shutouts,omitempty"`
|
||||||
|
ShutsAgainst *float64 `json:"shutouts_against,omitempty"`
|
||||||
|
HasMercyRuled *float64 `json:"has_mercy_ruled,omitempty"`
|
||||||
|
WasMercyRuled *float64 `json:"was_mercy_ruled,omitempty"`
|
||||||
|
PeriodsPlayed *float64 `json:"periods_played,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player represents a single player's data in a match
|
||||||
|
type Player struct {
|
||||||
|
GameUserID string `json:"game_user_id"`
|
||||||
|
Team string `json:"team"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Stats PlayerStats `json:"stats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GameStats contains the in-game statistics and settings for a match.
|
||||||
|
// This is the core match data returned both by the API and in local match logs.
|
||||||
|
type GameStats struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Arena string `json:"arena"`
|
||||||
|
Score Score `json:"score"`
|
||||||
|
Winner string `json:"winner"`
|
||||||
|
EndReason string `json:"end_reason"`
|
||||||
|
MatchLength string `json:"match_length"`
|
||||||
|
PeriodsEnabled string `json:"periods_enabled"`
|
||||||
|
CurrentPeriod string `json:"current_period"`
|
||||||
|
CustomMercyRule string `json:"custom_mercy_rule"`
|
||||||
|
Players []Player `json:"players"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game represents a full match as returned by the API /games/{id} endpoint
|
||||||
|
type Game struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
MatchType string `json:"match_type"`
|
||||||
|
Gamemode string `json:"gamemode"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
GameStats GameStats `json:"game_stats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LobbyPlayer represents a player in a lobby
|
||||||
|
type LobbyPlayer struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LobbyTeams represents the team assignments in a lobby
|
||||||
|
type LobbyTeams struct {
|
||||||
|
Home []LobbyPlayer `json:"home"`
|
||||||
|
Away []LobbyPlayer `json:"away"`
|
||||||
|
Spectators []LobbyPlayer `json:"spectators"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lobby represents a custom lobby as returned by the API
|
||||||
|
type Lobby struct {
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
HasPassword bool `json:"has_password"`
|
||||||
|
Owner int `json:"owner"`
|
||||||
|
OwnerName string `json:"owner_name"`
|
||||||
|
PlayerCount int `json:"player_count"`
|
||||||
|
MaxPlayers int `json:"max_players"`
|
||||||
|
InGame bool `json:"in_game"`
|
||||||
|
Players LobbyTeams `json:"players"`
|
||||||
|
MercyRule int `json:"mercy_rule"`
|
||||||
|
Arena string `json:"arena"`
|
||||||
|
PeriodsEnabled bool `json:"periods_enabled"`
|
||||||
|
CurrentPeriod int `json:"current_period"`
|
||||||
|
Score Score `json:"score"`
|
||||||
|
Starting bool `json:"starting"`
|
||||||
|
CanStart bool `json:"can_start"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LobbyCreationRequest contains the parameters for creating a new lobby
|
||||||
|
type LobbyCreationRequest struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
CreatorName string `json:"creator_name"`
|
||||||
|
|
||||||
|
// Optional fields
|
||||||
|
IsPeriods *bool `json:"is_periods,omitempty"`
|
||||||
|
CurrentPeriod *int `json:"current_period,omitempty"`
|
||||||
|
MatchLength *int `json:"match_length,omitempty"`
|
||||||
|
MercyRule *int `json:"mercy_rule,omitempty"`
|
||||||
|
Arena string `json:"arena,omitempty"`
|
||||||
|
|
||||||
|
InitialScore *Score `json:"initial_score,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LobbyCreationResponse is returned when a lobby is successfully created
|
||||||
|
type LobbyCreationResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
LobbyID string `json:"lobby_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchmakingPlayer represents a player in the matchmaking queue
|
||||||
|
type MatchmakingPlayer struct {
|
||||||
|
UUID int `json:"uuid"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchmakingRegion represents a region in a matchmaking entity
|
||||||
|
type MatchmakingRegion struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchmakingEntity represents a group/party in the matchmaking queue
|
||||||
|
type MatchmakingEntity struct {
|
||||||
|
Players []MatchmakingPlayer `json:"players"`
|
||||||
|
Regions []MatchmakingRegion `json:"regions"`
|
||||||
|
MMR int `json:"mmr"`
|
||||||
|
MMROffset int `json:"mmr_offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchmakingResponse is the full matchmaking queue response from the API
|
||||||
|
type MatchmakingResponse struct {
|
||||||
|
Entities []MatchmakingEntity `json:"entities"`
|
||||||
|
Playlists PubsQueue `json:"playlists"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user