added slapapi
This commit is contained in:
37
pkg/slapshotapi/client.go
Normal file
37
pkg/slapshotapi/client.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package slapshotapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type SlapAPI struct {
|
||||
client *http.Client
|
||||
ratelimiter *rate.Limiter
|
||||
mu sync.Mutex
|
||||
maxTokens int
|
||||
key string
|
||||
env string
|
||||
}
|
||||
|
||||
func NewSlapAPIClient(cfg *Config) (*SlapAPI, error) {
|
||||
if cfg == nil {
|
||||
return nil, errors.New("config cannot be nil")
|
||||
}
|
||||
if cfg.Environment != "api" && cfg.Environment != "staging" {
|
||||
return nil, errors.New("invalid env specified, must be 'api' or 'staging'")
|
||||
}
|
||||
rl := rate.NewLimiter(rate.Inf, 10)
|
||||
client := &SlapAPI{
|
||||
client: http.DefaultClient,
|
||||
ratelimiter: rl,
|
||||
mu: sync.Mutex{},
|
||||
maxTokens: 10,
|
||||
key: cfg.Key,
|
||||
env: cfg.Environment,
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
23
pkg/slapshotapi/config.go
Normal file
23
pkg/slapshotapi/config.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Package slapshotapi provides utilities for interacting with the slapshot public API
|
||||
package slapshotapi
|
||||
|
||||
import (
|
||||
"git.haelnorr.com/h/golib/env"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Environment string // ENV SLAPSHOT_ENVIRONMENT: API environment to connect to (default: staging)
|
||||
Key string // ENV SLAPSHOT_API_KEY: API Key for authorisation with the API (required)
|
||||
}
|
||||
|
||||
func ConfigFromEnv() (any, error) {
|
||||
cfg := &Config{
|
||||
Environment: env.String("SLAPSHOT_ENVIRONMENT", "staging"),
|
||||
Key: env.String("SLAPSHOT_API_KEY", ""),
|
||||
}
|
||||
if cfg.Key == "" {
|
||||
return nil, errors.New("Envar not set: SLAPSHOT_API_KEY")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
35
pkg/slapshotapi/enums.go
Normal file
35
pkg/slapshotapi/enums.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package slapshotapi
|
||||
|
||||
const (
|
||||
RegionEUWest = "eu-west"
|
||||
RegionNAEast = "na-east"
|
||||
RegionNACentral = "na-central"
|
||||
RegionNAWest = "na-west"
|
||||
RegionOCEEast = "oce-east"
|
||||
|
||||
ArenaSlapstadium = "Slapstadium"
|
||||
ArenaSlapville = "Slapville"
|
||||
ArenaSlapstadiumMini = "Slapstadium_mini"
|
||||
ArenaTableHockey = "Table_Hockey"
|
||||
ArenaColosseum = "Colosseum"
|
||||
ArenaSlapvilleJumbo = "Slapville_Jumbo"
|
||||
ArenaSlapstation = "Slapstation"
|
||||
ArenaSlapstadiumXL = "Slapstadium_XL"
|
||||
ArenaIsland = "Island"
|
||||
ArenaObstacles = "Obstacles"
|
||||
ArenaObstaclesXL = "Obstacles_XL"
|
||||
|
||||
EndReasonEndOfReg = "EndOfRegulation"
|
||||
EndReasonOvertime = "Overtime"
|
||||
EndReasonHomeTeamLeft = "HomeTeamLeft"
|
||||
EndReasonAwayTeamLeft = "AwayTeamLeft"
|
||||
EndReasonMercy = "MercyRule"
|
||||
EndReasonTie = "Tie"
|
||||
EndReasonForfeit = "Forfeit"
|
||||
EndReasonCancelled = "Cancelled"
|
||||
EndReasonUnknown = "Unknown"
|
||||
|
||||
GameModeHockey = "hockey"
|
||||
GameModeDodgePuck = "dodgepuck"
|
||||
GameModeTag = "tag"
|
||||
)
|
||||
41
pkg/slapshotapi/ezconf.go
Normal file
41
pkg/slapshotapi/ezconf.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package slapshotapi
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EZConfIntegration provides integration with ezconf for automatic configuration
|
||||
type EZConfIntegration struct {
|
||||
configFunc func() (any, error)
|
||||
name string
|
||||
}
|
||||
|
||||
// PackagePath returns the path to the config package for source parsing
|
||||
func (e EZConfIntegration) PackagePath() string {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
// Return directory of this file
|
||||
return filename[:len(filename)-len("/ezconf.go")]
|
||||
}
|
||||
|
||||
// ConfigFunc returns the ConfigFromEnv function for ezconf
|
||||
func (e EZConfIntegration) ConfigFunc() func() (any, error) {
|
||||
return func() (any, error) {
|
||||
return e.configFunc()
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the name to use when registering with ezconf
|
||||
func (e EZConfIntegration) Name() string {
|
||||
return strings.ToLower(e.name)
|
||||
}
|
||||
|
||||
// GroupName returns the display name for grouping environment variables
|
||||
func (e EZConfIntegration) GroupName() string {
|
||||
return e.name
|
||||
}
|
||||
|
||||
// NewEZConfIntegration creates a new EZConf integration helper
|
||||
func NewEZConfIntegration() EZConfIntegration {
|
||||
return EZConfIntegration{name: "SlapshotAPI", configFunc: ConfigFromEnv}
|
||||
}
|
||||
59
pkg/slapshotapi/limiting.go
Normal file
59
pkg/slapshotapi/limiting.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package slapshotapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
func (c *SlapAPI) do(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||
for {
|
||||
err := c.ratelimiter.Wait(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "c.ratelimiter.Wait")
|
||||
}
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "c.client.Do")
|
||||
}
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
resetAfter := 30 * time.Second
|
||||
err := resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "resp.Body.Close")
|
||||
}
|
||||
if resetAfter > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(resetAfter):
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
c.updateLimiterFromHeaders(resp.Header)
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SlapAPI) updateLimiterFromHeaders(h http.Header) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
limit, err1 := strconv.Atoi(h.Get("RateLimit-Limit"))
|
||||
window, err2 := strconv.Atoi(h.Get("RateLimit-Window"))
|
||||
|
||||
if err1 != nil || err2 != nil || limit <= 0 || window <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if limit != c.maxTokens || time.Duration(window) != time.Duration(float64(window)/float64(limit))*time.Second {
|
||||
c.maxTokens = limit
|
||||
c.ratelimiter.SetBurst(limit)
|
||||
c.ratelimiter.SetLimit(rate.Every(time.Duration(window) / time.Duration(limit)))
|
||||
}
|
||||
}
|
||||
62
pkg/slapshotapi/queuestatus.go
Normal file
62
pkg/slapshotapi/queuestatus.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package slapshotapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type endpointMatchmaking struct {
|
||||
regions []string
|
||||
}
|
||||
|
||||
func getEndpointMatchmaking(regions []string) *endpointMatchmaking {
|
||||
return &endpointMatchmaking{
|
||||
regions: regions,
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *endpointMatchmaking) path() string {
|
||||
path := "/api/public/matchmaking%s"
|
||||
filters := ""
|
||||
if len(ep.regions) > 0 {
|
||||
filters = "?regions="
|
||||
for i, region := range ep.regions {
|
||||
filters = filters + region
|
||||
if i+1 != len(ep.regions) {
|
||||
filters = filters + ","
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(path, filters)
|
||||
}
|
||||
|
||||
func (ep *endpointMatchmaking) method() string {
|
||||
return "GET"
|
||||
}
|
||||
|
||||
type matchmakingresp struct {
|
||||
Playlists PubsQueue `json:"playlists"`
|
||||
}
|
||||
|
||||
type PubsQueue struct {
|
||||
InQueue uint16 `json:"in_queue"`
|
||||
InMatch uint16 `json:"in_match"`
|
||||
}
|
||||
|
||||
// GetQueueStatus gets the number of players in public matchmaking
|
||||
func (c *SlapAPI) GetQueueStatus(
|
||||
ctx context.Context,
|
||||
regions []string,
|
||||
) (*PubsQueue, error) {
|
||||
endpoint := getEndpointMatchmaking(regions)
|
||||
data, err := c.request(ctx, endpoint)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "slapapiReq")
|
||||
}
|
||||
resp := matchmakingresp{}
|
||||
json.Unmarshal(data, &resp)
|
||||
return &resp.Playlists, nil
|
||||
}
|
||||
44
pkg/slapshotapi/request.go
Normal file
44
pkg/slapshotapi/request.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package slapshotapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type endpoint interface {
|
||||
path() string
|
||||
method() string
|
||||
}
|
||||
|
||||
func (c *SlapAPI) request(
|
||||
ctx context.Context,
|
||||
ep endpoint,
|
||||
) ([]byte, error) {
|
||||
baseurl := fmt.Sprintf("https://%s.slapshot.gg%s", c.env, ep.path())
|
||||
req, err := http.NewRequest(ep.method(), baseurl, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "http.NewRequest")
|
||||
}
|
||||
req.Header.Add("accept", "application/json")
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.key))
|
||||
res, err := c.do(ctx, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "http.DefaultClient.Do")
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return nil, errors.New(fmt.Sprintf("Error making request: %v", res.StatusCode))
|
||||
}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
49
pkg/slapshotapi/slapid.go
Normal file
49
pkg/slapshotapi/slapid.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package slapshotapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type endpointSteamID struct {
|
||||
steamID string
|
||||
}
|
||||
|
||||
func getEndpointSteamID(steamID string) *endpointSteamID {
|
||||
return &endpointSteamID{
|
||||
steamID: steamID,
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *endpointSteamID) path() string {
|
||||
return fmt.Sprintf("/api/public/players/steam/%s", ep.steamID)
|
||||
}
|
||||
|
||||
func (ep *endpointSteamID) method() string {
|
||||
return "GET"
|
||||
}
|
||||
|
||||
type idresp struct {
|
||||
ID uint32 `json:"id"`
|
||||
}
|
||||
|
||||
// GetSlapID returns the slapshot ID of the steam user
|
||||
func (c *SlapAPI) GetSlapID(
|
||||
ctx context.Context,
|
||||
steamid string,
|
||||
) (uint32, error) {
|
||||
endpoint := getEndpointSteamID(steamid)
|
||||
data, err := c.request(ctx, endpoint)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "slapapiReq")
|
||||
}
|
||||
resp := idresp{}
|
||||
err = json.Unmarshal(data, &resp)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "json.Unmarshal")
|
||||
}
|
||||
return resp.ID, nil
|
||||
}
|
||||
Reference in New Issue
Block a user