Initial commit

This commit is contained in:
rudder
2026-08-12 14:04:30 +03:00
commit 718eb09a78
34 changed files with 661 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="github.com/pkg/errors" />
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KubernetesApiProvider">{}</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/liveops-go-sdk.iml" filepath="$PROJECT_DIR$/.idea/liveops-go-sdk.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+55
View File
@@ -0,0 +1,55 @@
# liveops-go-sdk
Серверный Go SDK для LiveOps-платформы Rudder. Покрывает server-admin
поверхность `/game/v1` (auth по `X-API-Key`, ключ — `GAME_ADMIN_API_KEY`
на gateway).
## Install
Модуль раздаётся напрямую с self-hosted Gitea, минуя proxy.golang.org,
поэтому Go должен пропускать его мимо checksum database:
```bash
go env -w GOPRIVATE=hub.rudder.build/*
go get hub.rudder.build/rudder/rudder-go-sdk@latest
```
Требуется Go 1.25+. Токен не нужен — репозиторий публичный.
## Usage
```go
client, err := rudder.New(rudder.Config{
APIKey: os.Getenv("GAME_ADMIN_API_KEY"),
ProjectID: "your-project-id",
})
if err != nil {
// *rudder.APIError с кодом sdk/invalid-options
}
players, err := client.Players.List(ctx, rudder.ListPlayersParameters{Limit: 100})
entries, err := client.Leaderboards.Results(ctx, "weekly")
wallet, err := client.Wallet.Adjust(ctx, playerID, rudder.AdjustWalletParameters{
CurrencyCode: "coins",
Amount: 100,
Reason: "compensation",
})
metrics, err := client.Metrics.Get(ctx)
page, err := client.Ugc.List(ctx, rudder.ListUgcParameters{Status: "pending"})
```
## Ошибки
```go
var apiErr *rudder.APIError
if errors.As(err, &apiErr) {
apiErr.Status // HTTP status
apiErr.Code // машинный код, константы в models.ErrorCode*
apiErr.RequestID // корреляция с логами gateway
}
```
## Генерация
`models/` генерируется apigen'ом из `liveops-gateway/openapi.yaml`
(`make generate-openapi` в liveops-gateway). Руками не править.
+58
View File
@@ -0,0 +1,58 @@
package rudder
import (
"net/http"
"time"
)
const DefaultBaseURL = "https://api.rudder.build"
type Config struct {
APIKey string
ProjectID string
BaseURL string
Timeout time.Duration
HTTPClient *http.Client
}
type Client struct {
Players *PlayersService
Metrics *MetricsService
Leaderboards *LeaderboardsService
Wallet *WalletService
Ugc *UgcService
}
func New(config Config) (*Client, error) {
if config.APIKey == "" {
return nil, &APIError{Code: ErrorCodeInvalidOptions, Message: "APIKey is required"}
}
if config.ProjectID == "" {
return nil, &APIError{Code: ErrorCodeInvalidOptions, Message: "ProjectID is required"}
}
if config.BaseURL == "" {
config.BaseURL = DefaultBaseURL
}
if config.Timeout == 0 {
config.Timeout = 30 * time.Second
}
if config.HTTPClient == nil {
config.HTTPClient = &http.Client{Timeout: config.Timeout}
}
transport := &transport{
baseURL: config.BaseURL,
apiKey: config.APIKey,
projectID: config.ProjectID,
httpClient: config.HTTPClient,
}
return &Client{
Players: &PlayersService{transport: transport},
Metrics: &MetricsService{transport: transport},
Leaderboards: &LeaderboardsService{transport: transport},
Wallet: &WalletService{transport: transport},
Ugc: &UgcService{transport: transport},
}, nil
}
+19
View File
@@ -0,0 +1,19 @@
package rudder
import "fmt"
const ErrorCodeInvalidOptions = "sdk/invalid-options"
type APIError struct {
Status int
Code string
Message string
RequestID string
}
func (e *APIError) Error() string {
if e.RequestID != "" {
return fmt.Sprintf("rudder: status %d, code %q: %s (request %s)", e.Status, e.Code, e.Message, e.RequestID)
}
return fmt.Sprintf("rudder: status %d, code %q: %s", e.Status, e.Code, e.Message)
}
+3
View File
@@ -0,0 +1,3 @@
module hub.rudder.build/rudder/rudder-go-sdk
go 1.25.2
+21
View File
@@ -0,0 +1,21 @@
package rudder
import (
"context"
"net/http"
"net/url"
"hub.rudder.build/rudder/rudder-go-sdk/models"
)
type LeaderboardsService struct {
transport *transport
}
func (s *LeaderboardsService) Results(ctx context.Context, slug string) ([]models.RankEntry, error) {
var response models.GetLeaderboardResultsResponse
if err := s.transport.do(ctx, http.MethodGet, "/leaderboards/"+url.PathEscape(slug)+"/results", nil, nil, &response); err != nil {
return nil, err
}
return response.Entries, nil
}
+20
View File
@@ -0,0 +1,20 @@
package rudder
import (
"context"
"net/http"
"hub.rudder.build/rudder/rudder-go-sdk/models"
)
type MetricsService struct {
transport *transport
}
func (s *MetricsService) Get(ctx context.Context) (*models.GetProjectMetricsResponse, error) {
var response models.GetProjectMetricsResponse
if err := s.transport.do(ctx, http.MethodGet, "/metrics", nil, nil, &response); err != nil {
return nil, err
}
return &response, nil
}
+10
View File
@@ -0,0 +1,10 @@
// Code generated by apigen. DO NOT EDIT.
package models
type AdjustPlayerWalletRequest struct {
Amount int64 `json:"amount,omitempty"`
CurrencyCode string `json:"currencyCode,omitempty"`
PlayerID string `json:"playerId,omitempty"`
Reason string `json:"reason,omitempty"`
}
+19
View File
@@ -0,0 +1,19 @@
// Code generated by apigen. DO NOT EDIT.
package models
import "encoding/json"
type AdminPlayer struct {
CreatedAt string `json:"createdAt,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
ID string `json:"id,omitempty"`
Language string `json:"language,omitempty"`
LastSeen string `json:"lastSeen,omitempty"`
Nickname string `json:"nickname,omitempty"`
PayerFlag bool `json:"payerFlag,omitempty"`
ProjectID string `json:"projectId,omitempty"`
Region string `json:"region,omitempty"`
StableID string `json:"stableId,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
+11
View File
@@ -0,0 +1,11 @@
// Code generated by apigen. DO NOT EDIT.
package models
type AdminPlayerDetails struct {
Inventory []InventoryItem `json:"inventory,omitempty"`
Player AdminPlayer `json:"player,omitempty"`
Storages []PlayerStorage `json:"storages,omitempty"`
WalletTransactions []WalletAudit `json:"walletTransactions,omitempty"`
Wallets []WalletDetails `json:"wallets,omitempty"`
}
+15
View File
@@ -0,0 +1,15 @@
// Code generated by apigen. DO NOT EDIT.
package models
const (
ErrorCodeEarlyCompletion = "early_completion"
ErrorCodeForbidden = "forbidden"
ErrorCodeLevelNotReached = "level_not_reached"
ErrorCodeNodeNotActive = "node_not_active"
ErrorCodeObjectivesIncomplete = "objectives_incomplete"
ErrorCodeRunExpired = "run_expired"
ErrorCodeRunNotActive = "run_not_active"
ErrorCodeScenarioNotActive = "scenario_not_active"
ErrorCodeUnknownRun = "unknown_run"
)
+7
View File
@@ -0,0 +1,7 @@
// Code generated by apigen. DO NOT EDIT.
package models
type GetLeaderboardResultsResponse struct {
Entries []RankEntry `json:"entries,omitempty"`
}
+11
View File
@@ -0,0 +1,11 @@
// Code generated by apigen. DO NOT EDIT.
package models
type GetProjectMetricsResponse struct {
Dau int `json:"dau,omitempty"`
DauChange float64 `json:"dauChange,omitempty"`
Mau int `json:"mau,omitempty"`
MauChange float64 `json:"mauChange,omitempty"`
Retention []RetentionPoint `json:"retention,omitempty"`
}
+10
View File
@@ -0,0 +1,10 @@
// Code generated by apigen. DO NOT EDIT.
package models
type InventoryItem struct {
Amount int64 `json:"amount,omitempty"`
ID string `json:"id,omitempty"`
Slug string `json:"slug,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
+10
View File
@@ -0,0 +1,10 @@
// Code generated by apigen. DO NOT EDIT.
package models
type ListPlayersResponse struct {
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
Players []AdminPlayer `json:"players,omitempty"`
Total int `json:"total,omitempty"`
}
+8
View File
@@ -0,0 +1,8 @@
// Code generated by apigen. DO NOT EDIT.
package models
type ListUgcResponse struct {
Items []UgcSubmission `json:"items,omitempty"`
NextCursor string `json:"nextCursor,omitempty"`
}
+12
View File
@@ -0,0 +1,12 @@
// Code generated by apigen. DO NOT EDIT.
package models
type PlayerStorage struct {
Data string `json:"data,omitempty"`
ExpiresAt string `json:"expiresAt,omitempty"`
ID string `json:"id,omitempty"`
PlayerID string `json:"playerId,omitempty"`
Type string `json:"type,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
+10
View File
@@ -0,0 +1,10 @@
// Code generated by apigen. DO NOT EDIT.
package models
type RankEntry struct {
PlayerID string `json:"playerId,omitempty"`
PlayerName string `json:"playerName,omitempty"`
Rank int64 `json:"rank,omitempty"`
Score float64 `json:"score,omitempty"`
}
+8
View File
@@ -0,0 +1,8 @@
// Code generated by apigen. DO NOT EDIT.
package models
type RetentionPoint struct {
Day int `json:"day,omitempty"`
Value float64 `json:"value,omitempty"`
}
+9
View File
@@ -0,0 +1,9 @@
// Code generated by apigen. DO NOT EDIT.
package models
type ReviewUgcRequest struct {
ID string `json:"id,omitempty"`
ReviewedBy string `json:"reviewedBy,omitempty"`
Status string `json:"status,omitempty"`
}
+8
View File
@@ -0,0 +1,8 @@
// Code generated by apigen. DO NOT EDIT.
package models
type SubmittedBy struct {
UserID string `json:"userId,omitempty"`
Username string `json:"username,omitempty"`
}
+19
View File
@@ -0,0 +1,19 @@
// Code generated by apigen. DO NOT EDIT.
package models
import "encoding/json"
type UgcSubmission struct {
Description string `json:"description,omitempty"`
FileSize int64 `json:"fileSize,omitempty"`
FileURL string `json:"fileUrl,omitempty"`
ID string `json:"id,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
Name string `json:"name,omitempty"`
ReviewedAt string `json:"reviewedAt,omitempty"`
ReviewedBy string `json:"reviewedBy,omitempty"`
Status string `json:"status,omitempty"`
SubmittedAt string `json:"submittedAt,omitempty"`
SubmittedBy SubmittedBy `json:"submittedBy,omitempty"`
}
+10
View File
@@ -0,0 +1,10 @@
// Code generated by apigen. DO NOT EDIT.
package models
import "encoding/json"
type UpdateUgcMetadataRequest struct {
ID string `json:"id,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
+19
View File
@@ -0,0 +1,19 @@
// Code generated by apigen. DO NOT EDIT.
package models
import "encoding/json"
type WalletAudit struct {
Amount int64 `json:"amount,omitempty"`
BalanceAfter int64 `json:"balanceAfter,omitempty"`
BalanceBefore int64 `json:"balanceBefore,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
CurrencyCode string `json:"currencyCode,omitempty"`
ID string `json:"id,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
PlayerID string `json:"playerId,omitempty"`
Reason string `json:"reason,omitempty"`
Source string `json:"source,omitempty"`
WalletID string `json:"walletId,omitempty"`
}
+11
View File
@@ -0,0 +1,11 @@
// Code generated by apigen. DO NOT EDIT.
package models
type WalletDetails struct {
Balance int64 `json:"balance,omitempty"`
CurrencyCode string `json:"currencyCode,omitempty"`
ID string `json:"id,omitempty"`
PlayerID string `json:"playerId,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
+43
View File
@@ -0,0 +1,43 @@
package rudder
import (
"context"
"net/http"
"net/url"
"strconv"
"hub.rudder.build/rudder/rudder-go-sdk/models"
)
type PlayersService struct {
transport *transport
}
type ListPlayersParameters struct {
Limit int
Offset int
}
func (s *PlayersService) List(ctx context.Context, parameters ListPlayersParameters) (*models.ListPlayersResponse, error) {
query := url.Values{}
if parameters.Limit > 0 {
query.Set("limit", strconv.Itoa(parameters.Limit))
}
if parameters.Offset > 0 {
query.Set("offset", strconv.Itoa(parameters.Offset))
}
var response models.ListPlayersResponse
if err := s.transport.do(ctx, http.MethodGet, "/players/list", query, nil, &response); err != nil {
return nil, err
}
return &response, nil
}
func (s *PlayersService) Get(ctx context.Context, playerID string) (*models.AdminPlayerDetails, error) {
var response models.AdminPlayerDetails
if err := s.transport.do(ctx, http.MethodGet, "/players/"+url.PathEscape(playerID), nil, nil, &response); err != nil {
return nil, err
}
return &response, nil
}
+81
View File
@@ -0,0 +1,81 @@
package rudder
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type transport struct {
baseURL string
apiKey string
projectID string
httpClient *http.Client
}
func (t *transport) do(ctx context.Context, method, path string, query url.Values, body any, out any) error {
endpoint := strings.TrimSuffix(t.baseURL, "/") + "/game/v1/projects/" + t.projectID + path
if len(query) > 0 {
endpoint += "?" + query.Encode()
}
var reader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("rudder: encode request body: %w", err)
}
reader = bytes.NewReader(data)
}
req, err := http.NewRequestWithContext(ctx, method, endpoint, reader)
if err != nil {
return fmt.Errorf("rudder: build request: %w", err)
}
req.Header.Set("X-API-Key", t.apiKey)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := t.httpClient.Do(req)
if err != nil {
return fmt.Errorf("rudder: %s %s: %w", method, path, err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("rudder: read response body: %w", err)
}
if resp.StatusCode >= 400 {
apiErr := &APIError{Status: resp.StatusCode}
var payload struct {
Error string `json:"error"`
Code string `json:"code"`
RequestID string `json:"requestId"`
}
if json.Unmarshal(data, &payload) == nil {
apiErr.Code = payload.Code
apiErr.Message = payload.Error
apiErr.RequestID = payload.RequestID
}
if apiErr.Message == "" {
apiErr.Message = http.StatusText(resp.StatusCode)
}
return apiErr
}
if out == nil || len(data) == 0 {
return nil
}
if err := json.Unmarshal(data, out); err != nil {
return fmt.Errorf("rudder: decode response: %w", err)
}
return nil
}
+72
View File
@@ -0,0 +1,72 @@
package rudder
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strconv"
"hub.rudder.build/rudder/rudder-go-sdk/models"
)
type UgcService struct {
transport *transport
}
type ListUgcParameters struct {
Status string
Limit int
Cursor string
}
func (s *UgcService) List(ctx context.Context, parameters ListUgcParameters) (*models.ListUgcResponse, error) {
query := url.Values{}
if parameters.Status != "" {
query.Set("status", parameters.Status)
}
if parameters.Limit > 0 {
query.Set("limit", strconv.Itoa(parameters.Limit))
}
if parameters.Cursor != "" {
query.Set("cursor", parameters.Cursor)
}
var response models.ListUgcResponse
if err := s.transport.do(ctx, http.MethodGet, "/ugc", query, nil, &response); err != nil {
return nil, err
}
return &response, nil
}
func (s *UgcService) UpdateMetadata(ctx context.Context, id string, metadata json.RawMessage) (*models.UgcSubmission, error) {
body := models.UpdateUgcMetadataRequest{
ID: id,
Metadata: metadata,
}
var response models.UgcSubmission
if err := s.transport.do(ctx, http.MethodPatch, "/ugc/"+url.PathEscape(id)+"/metadata", nil, body, &response); err != nil {
return nil, err
}
return &response, nil
}
type ReviewUgcParameters struct {
Status string
ReviewedBy string
}
func (s *UgcService) Review(ctx context.Context, id string, parameters ReviewUgcParameters) (*models.UgcSubmission, error) {
body := models.ReviewUgcRequest{
ID: id,
Status: parameters.Status,
ReviewedBy: parameters.ReviewedBy,
}
var response models.UgcSubmission
if err := s.transport.do(ctx, http.MethodPost, "/ugc/"+url.PathEscape(id)+"/review", nil, body, &response); err != nil {
return nil, err
}
return &response, nil
}
+34
View File
@@ -0,0 +1,34 @@
package rudder
import (
"context"
"net/http"
"net/url"
"hub.rudder.build/rudder/rudder-go-sdk/models"
)
type WalletService struct {
transport *transport
}
type AdjustWalletParameters struct {
CurrencyCode string
Amount int64
Reason string
}
func (s *WalletService) Adjust(ctx context.Context, playerID string, parameters AdjustWalletParameters) (*models.WalletDetails, error) {
body := models.AdjustPlayerWalletRequest{
PlayerID: playerID,
CurrencyCode: parameters.CurrencyCode,
Amount: parameters.Amount,
Reason: parameters.Reason,
}
var response models.WalletDetails
if err := s.transport.do(ctx, http.MethodPost, "/players/"+url.PathEscape(playerID)+"/wallet/adjust", nil, body, &response); err != nil {
return nil, err
}
return &response, nil
}