package companion import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "regexp" "strings" "time" ) const semanticEventsSnapshotType = "semantic.events" const semanticEventsSchemaVersion = "1" const semanticEventsRetentionSeconds = 7 * 24 * 60 * 60 const semanticEventsMaxRecords = 1000 type SemanticEvent struct { Type string `json:"type"`; OccurredAt time.Time `json:"occurredAt"`; PlayerID string `json:"playerId,omitempty"`; DisplayName string `json:"displayName,omitempty"`; NetworkCorrelation string `json:"networkCorrelation,omitempty"` } var supportedLoginLine = regexp.MustCompile(`^LOGIN player=([A-Za-z0-9._:-]{1,96}) name=([^\n]{1,80}) at=([0-9TZ:+.-]{20,40})(?: network=([^\s]{1,128}))?$`) var supportedLogoutLine = regexp.MustCompile(`^LOGOUT player=([A-Za-z0-9._:-]{1,96}) at=([0-9TZ:+.-]{20,40})$`) // ParseSemanticEvent supports only versioned, allow-listed extension output. // Unknown formats deliberately yield no event and may be reported as a bounded // diagnostic by the caller. func ParseSemanticEvent(line string, serverCorrelationKey []byte) (SemanticEvent, bool) { if match := supportedLoginLine.FindStringSubmatch(strings.TrimSpace(line)); len(match) != 0 { occurred, err := time.Parse(time.RFC3339, match[3]); if err != nil { return SemanticEvent{}, false }; event := SemanticEvent{Type: "scum.login", PlayerID: match[1], DisplayName: match[2], OccurredAt: occurred}; if match[4] != "" { event.NetworkCorrelation = irreversibleServerCorrelation(serverCorrelationKey, match[4]) }; return event, true } if match := supportedLogoutLine.FindStringSubmatch(strings.TrimSpace(line)); len(match) != 0 { occurred, err := time.Parse(time.RFC3339, match[2]); if err != nil { return SemanticEvent{}, false }; return SemanticEvent{Type: "scum.logout", PlayerID: match[1], OccurredAt: occurred}, true } return SemanticEvent{}, false } func irreversibleServerCorrelation(key []byte, source string) string { if len(key) == 0 || source == "" { return "" }; mac := hmac.New(sha256.New, key); _, _ = mac.Write([]byte(source)); return hex.EncodeToString(mac.Sum(nil)) } func (client *Client) UploadSemanticEvents(ctx context.Context, streamKey string, sequence uint64, events []SemanticEvent) (AcceptedSnapshot, error) { if len(events) == 0 || len(events) > 100 { return AcceptedSnapshot{}, fmt.Errorf("semantic event batch is invalid") }; payload := map[string]any{"events": events}; return client.UploadSnapshot(ctx, Snapshot{Type: semanticEventsSnapshotType, SchemaVersion: semanticEventsSchemaVersion, StreamKey: streamKey, Sequence: sequence, ObservedAt: client.now().UTC(), Payload: payload, KeepForSeconds: semanticEventsRetentionSeconds, MaxRecords: semanticEventsMaxRecords}) }