added slapapi
This commit is contained in:
@@ -10,19 +10,21 @@ import (
|
||||
"git.haelnorr.com/h/oslstats/internal/discord"
|
||||
"git.haelnorr.com/h/oslstats/internal/rbac"
|
||||
"git.haelnorr.com/h/oslstats/pkg/oauth"
|
||||
"git.haelnorr.com/h/oslstats/pkg/slapshotapi"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DB *db.Config
|
||||
HWS *hws.Config
|
||||
HWSAuth *hwsauth.Config
|
||||
HLOG *hlog.Config
|
||||
Discord *discord.Config
|
||||
OAuth *oauth.Config
|
||||
RBAC *rbac.Config
|
||||
Flags *Flags
|
||||
DB *db.Config
|
||||
HWS *hws.Config
|
||||
HWSAuth *hwsauth.Config
|
||||
HLOG *hlog.Config
|
||||
Discord *discord.Config
|
||||
OAuth *oauth.Config
|
||||
RBAC *rbac.Config
|
||||
Slapshot *slapshotapi.Config
|
||||
Flags *Flags
|
||||
}
|
||||
|
||||
// GetConfig loads the application configuration and returns a pointer to the Config object
|
||||
@@ -42,6 +44,7 @@ func GetConfig(flags *Flags) (*Config, *ezconf.ConfigLoader, error) {
|
||||
discord.NewEZConfIntegration(),
|
||||
oauth.NewEZConfIntegration(),
|
||||
rbac.NewEZConfIntegration(),
|
||||
slapshotapi.NewEZConfIntegration(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "loader.RegisterIntegrations")
|
||||
@@ -93,15 +96,21 @@ func GetConfig(flags *Flags) (*Config, *ezconf.ConfigLoader, error) {
|
||||
return nil, nil, errors.New("RBAC Config not loaded")
|
||||
}
|
||||
|
||||
slapcfg, ok := loader.GetConfig("slapshotapi")
|
||||
if !ok {
|
||||
return nil, nil, errors.New("SlapshotAPI Config not loaded")
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
DB: dbcfg.(*db.Config),
|
||||
HWS: hwscfg.(*hws.Config),
|
||||
HWSAuth: hwsauthcfg.(*hwsauth.Config),
|
||||
HLOG: hlogcfg.(*hlog.Config),
|
||||
Discord: discordcfg.(*discord.Config),
|
||||
OAuth: oauthcfg.(*oauth.Config),
|
||||
RBAC: rbaccfg.(*rbac.Config),
|
||||
Flags: flags,
|
||||
DB: dbcfg.(*db.Config),
|
||||
HWS: hwscfg.(*hws.Config),
|
||||
HWSAuth: hwsauthcfg.(*hwsauth.Config),
|
||||
HLOG: hlogcfg.(*hlog.Config),
|
||||
Discord: discordcfg.(*discord.Config),
|
||||
OAuth: oauthcfg.(*oauth.Config),
|
||||
RBAC: rbaccfg.(*rbac.Config),
|
||||
Slapshot: slapcfg.(*slapshotapi.Config),
|
||||
Flags: flags,
|
||||
}
|
||||
|
||||
return config, loader, nil
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package discord provides utilities for interacting with the discord API
|
||||
package discord
|
||||
|
||||
import (
|
||||
|
||||
@@ -19,19 +19,19 @@ type RateLimitState struct {
|
||||
|
||||
// Do executes an HTTP request with automatic rate limit handling
|
||||
// It will wait if rate limits are about to be exceeded and retry once if a 429 is received
|
||||
func (c *APIClient) Do(req *http.Request) (*http.Response, error) {
|
||||
func (api *APIClient) Do(req *http.Request) (*http.Response, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New("request cannot be nil")
|
||||
}
|
||||
|
||||
// Step 1: Check if we need to wait before making request
|
||||
bucket := c.getBucketFromRequest(req)
|
||||
if err := c.waitIfNeeded(bucket); err != nil {
|
||||
bucket := api.getBucketFromRequest(req)
|
||||
if err := api.waitIfNeeded(bucket); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Step 2: Execute request
|
||||
resp, err := c.client.Do(req)
|
||||
resp, err := api.client.Do(req)
|
||||
if err != nil {
|
||||
// Check if it's a network timeout
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
@@ -41,17 +41,17 @@ func (c *APIClient) Do(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
// Step 3: Update rate limit state from response headers
|
||||
c.updateRateLimit(resp.Header)
|
||||
api.updateRateLimit(resp.Header)
|
||||
|
||||
// Step 4: Handle 429 (rate limited)
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
resp.Body.Close() // Close original response
|
||||
|
||||
retryAfter := c.parseRetryAfter(resp.Header)
|
||||
retryAfter := api.parseRetryAfter(resp.Header)
|
||||
|
||||
// No Retry-After header, can't retry safely
|
||||
if retryAfter == 0 {
|
||||
c.logger.Warn().
|
||||
api.logger.Warn().
|
||||
Str("bucket", bucket).
|
||||
Str("method", req.Method).
|
||||
Str("path", req.URL.Path).
|
||||
@@ -61,7 +61,7 @@ func (c *APIClient) Do(req *http.Request) (*http.Response, error) {
|
||||
|
||||
// Retry-After exceeds 30 second cap
|
||||
if retryAfter > 30*time.Second {
|
||||
c.logger.Warn().
|
||||
api.logger.Warn().
|
||||
Str("bucket", bucket).
|
||||
Str("method", req.Method).
|
||||
Str("path", req.URL.Path).
|
||||
@@ -74,7 +74,7 @@ func (c *APIClient) Do(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
// Wait and retry
|
||||
c.logger.Warn().
|
||||
api.logger.Warn().
|
||||
Str("bucket", bucket).
|
||||
Str("method", req.Method).
|
||||
Str("path", req.URL.Path).
|
||||
@@ -84,7 +84,7 @@ func (c *APIClient) Do(req *http.Request) (*http.Response, error) {
|
||||
time.Sleep(retryAfter)
|
||||
|
||||
// Retry the request
|
||||
resp, err = c.client.Do(req)
|
||||
resp, err = api.client.Do(req)
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
return nil, errors.Wrap(err, "retry request timed out")
|
||||
@@ -93,12 +93,12 @@ func (c *APIClient) Do(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
// Update rate limit again after retry
|
||||
c.updateRateLimit(resp.Header)
|
||||
api.updateRateLimit(resp.Header)
|
||||
|
||||
// If STILL rate limited after retry, return error
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
resp.Body.Close()
|
||||
c.logger.Error().
|
||||
api.logger.Error().
|
||||
Str("bucket", bucket).
|
||||
Str("method", req.Method).
|
||||
Str("path", req.URL.Path).
|
||||
@@ -115,15 +115,15 @@ func (c *APIClient) Do(req *http.Request) (*http.Response, error) {
|
||||
|
||||
// getBucketFromRequest extracts or generates bucket ID from request
|
||||
// For Discord, the bucket is typically METHOD:path until we get the actual bucket from headers
|
||||
func (c *APIClient) getBucketFromRequest(req *http.Request) string {
|
||||
func (api *APIClient) getBucketFromRequest(req *http.Request) string {
|
||||
return req.Method + ":" + req.URL.Path
|
||||
}
|
||||
|
||||
// waitIfNeeded checks if we need to delay before request to avoid hitting rate limits
|
||||
func (c *APIClient) waitIfNeeded(bucket string) error {
|
||||
c.mu.RLock()
|
||||
state, exists := c.buckets[bucket]
|
||||
c.mu.RUnlock()
|
||||
func (api *APIClient) waitIfNeeded(bucket string) error {
|
||||
api.mu.RLock()
|
||||
state, exists := api.buckets[bucket]
|
||||
api.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil // No state yet, proceed
|
||||
@@ -138,7 +138,7 @@ func (c *APIClient) waitIfNeeded(bucket string) error {
|
||||
waitDuration += 100 * time.Millisecond
|
||||
|
||||
if waitDuration > 0 {
|
||||
c.logger.Debug().
|
||||
api.logger.Debug().
|
||||
Str("bucket", bucket).
|
||||
Dur("wait_duration", waitDuration).
|
||||
Msg("Proactively waiting for rate limit reset")
|
||||
@@ -150,16 +150,16 @@ func (c *APIClient) waitIfNeeded(bucket string) error {
|
||||
}
|
||||
|
||||
// updateRateLimit parses response headers and updates bucket state
|
||||
func (c *APIClient) updateRateLimit(headers http.Header) {
|
||||
func (api *APIClient) updateRateLimit(headers http.Header) {
|
||||
bucket := headers.Get("X-RateLimit-Bucket")
|
||||
if bucket == "" {
|
||||
return // No bucket info, can't track
|
||||
}
|
||||
|
||||
// Parse headers
|
||||
limit := c.parseInt(headers.Get("X-RateLimit-Limit"))
|
||||
remaining := c.parseInt(headers.Get("X-RateLimit-Remaining"))
|
||||
resetAfter := c.parseFloat(headers.Get("X-RateLimit-Reset-After"))
|
||||
limit := api.parseInt(headers.Get("X-RateLimit-Limit"))
|
||||
remaining := api.parseInt(headers.Get("X-RateLimit-Remaining"))
|
||||
resetAfter := api.parseFloat(headers.Get("X-RateLimit-Reset-After"))
|
||||
|
||||
state := &RateLimitState{
|
||||
Bucket: bucket,
|
||||
@@ -168,12 +168,12 @@ func (c *APIClient) updateRateLimit(headers http.Header) {
|
||||
Reset: time.Now().Add(time.Duration(resetAfter * float64(time.Second))),
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.buckets[bucket] = state
|
||||
c.mu.Unlock()
|
||||
api.mu.Lock()
|
||||
api.buckets[bucket] = state
|
||||
api.mu.Unlock()
|
||||
|
||||
// Log rate limit state for debugging
|
||||
c.logger.Debug().
|
||||
api.logger.Debug().
|
||||
Str("bucket", bucket).
|
||||
Int("remaining", remaining).
|
||||
Int("limit", limit).
|
||||
@@ -182,14 +182,14 @@ func (c *APIClient) updateRateLimit(headers http.Header) {
|
||||
}
|
||||
|
||||
// parseRetryAfter extracts retry delay from Retry-After header
|
||||
func (c *APIClient) parseRetryAfter(headers http.Header) time.Duration {
|
||||
func (api *APIClient) parseRetryAfter(headers http.Header) time.Duration {
|
||||
retryAfter := headers.Get("Retry-After")
|
||||
if retryAfter == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Discord returns seconds as float
|
||||
seconds := c.parseFloat(retryAfter)
|
||||
seconds := api.parseFloat(retryAfter)
|
||||
if seconds <= 0 {
|
||||
return 0
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func (c *APIClient) parseRetryAfter(headers http.Header) time.Duration {
|
||||
}
|
||||
|
||||
// parseInt parses an integer from a header value, returns 0 on error
|
||||
func (c *APIClient) parseInt(s string) int {
|
||||
func (api *APIClient) parseInt(s string) int {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
@@ -207,7 +207,7 @@ func (c *APIClient) parseInt(s string) int {
|
||||
}
|
||||
|
||||
// parseFloat parses a float from a header value, returns 0 on error
|
||||
func (c *APIClient) parseFloat(s string) float64 {
|
||||
func (api *APIClient) parseFloat(s string) float64 {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
18
internal/discord/steamid.go
Normal file
18
internal/discord/steamid.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *OAuthSession) GetSteamID() (string, error) {
|
||||
connections, err := s.UserConnections()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "s.UserConnections")
|
||||
}
|
||||
for _, conn := range connections {
|
||||
if conn.Type == "steam" {
|
||||
return conn.ID, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("steam connection not found")
|
||||
}
|
||||
Reference in New Issue
Block a user