Make SCUM logs live relay only

This commit is contained in:
npc0-hue
2026-09-01 14:26:00 +08:00
parent 3f348401a9
commit 7d9c1b7e9e
28 changed files with 1011 additions and 175 deletions
@@ -0,0 +1,125 @@
package companion
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
const logEventsPath = "/api/v1/game-client-bridge/companion/logs/events"
type LogEntry struct {
Seq uint64 `json:"seq"`
Timestamp time.Time `json:"timestamp"`
Level string `json:"level,omitempty"`
Line string `json:"line"`
Fields map[string]string `json:"fields,omitempty"`
Redacted bool `json:"redacted"`
}
type LogStreamEvent struct {
ServerInstanceID string `json:"serverInstanceId"`
StreamID string `json:"streamId"`
Source string `json:"source"`
StreamKey string `json:"streamKey"`
LogSessionID string `json:"logSessionId,omitempty"`
SessionStartedAt time.Time `json:"sessionStartedAt,omitempty"`
LatestSeq uint64 `json:"latestSeq"`
Entry LogEntry `json:"entry"`
}
type logStreamRequest struct {
SessionToken string `json:"sessionToken"`
}
func (client *Client) StreamLogEvents(ctx context.Context, handle func(LogStreamEvent) error) error {
if handle == nil {
return fmt.Errorf("log event handler is required")
}
token, err := client.currentSession()
if err != nil {
return err
}
encoded, err := json.Marshal(logStreamRequest{SessionToken: token})
if err != nil {
return fmt.Errorf("encode log stream request: %w", err)
}
if len(encoded) > maxRequestBytes {
return fmt.Errorf("platform request exceeds the bounded payload size")
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, client.config.Platform.BaseURL+logEventsPath, bytes.NewReader(encoded))
if err != nil {
return fmt.Errorf("create log stream request: %w", err)
}
request.Header.Set("Accept", "text/event-stream")
request.Header.Set("Content-Type", "application/json")
response, err := client.httpClient.Do(request)
if err != nil {
return fmt.Errorf("open log stream: %w", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
return HTTPError{StatusCode: response.StatusCode, ExpectedStatus: http.StatusOK}
}
return readLogEventStream(ctx, response.Body, handle)
}
func readLogEventStream(ctx context.Context, body io.Reader, handle func(LogStreamEvent) error) error {
reader := bufio.NewReader(body)
var eventName string
var dataLines []string
flush := func() error {
if len(dataLines) == 0 {
eventName = ""
return nil
}
name := eventName
if name == "" {
name = "message"
}
payload := strings.Join(dataLines, "\n")
eventName = ""
dataLines = nil
if name != "log" {
return nil
}
var event LogStreamEvent
if err := json.Unmarshal([]byte(payload), &event); err != nil {
return fmt.Errorf("decode log event: %w", err)
}
return handle(event)
}
for {
line, err := reader.ReadString('\n')
if len(line) > 0 {
line = strings.TrimRight(line, "\r\n")
switch {
case line == "":
if flushErr := flush(); flushErr != nil {
return flushErr
}
case strings.HasPrefix(line, ":"):
case strings.HasPrefix(line, "event:"):
eventName = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
case strings.HasPrefix(line, "data:"):
dataLines = append(dataLines, strings.TrimSpace(strings.TrimPrefix(line, "data:")))
}
}
if err != nil {
if ctx != nil && ctx.Err() != nil {
return ctx.Err()
}
if err == io.EOF {
return flush()
}
return fmt.Errorf("read log stream: %w", err)
}
}
}