package companion import ( "context" "crypto/sha256" "encoding/hex" "fmt" "strings" "time" ) func (store *SCUMSQLStore) StoreConsoleRecords(ctx context.Context, records []ConsoleRecord) (int, error) { if store == nil || store.db == nil { return 0, fmt.Errorf("SCUM plugin SQL store is not configured") } normalized := make([]ConsoleRecord, 0, len(records)) for _, record := range records { value, err := normalizeConsoleRecord(record) if err != nil { return 0, err } normalized = append(normalized, value) } if len(normalized) == 0 { return 0, nil } tx, err := store.db.BeginTx(ctx, nil) if err != nil { return 0, fmt.Errorf("begin SCUM console log write: %w", err) } defer tx.Rollback() stamp := time.Now().UTC() for _, record := range normalized { if _, err := tx.ExecContext(ctx, scumConsoleLogInsertSQL, scumConsoleRecordKey(record), record.ServerID, record.Stream, record.Sequence, record.OccurredAt, record.Text, stamp, stamp); err != nil { return 0, fmt.Errorf("write SCUM console log: %w", err) } } if err := tx.Commit(); err != nil { return 0, fmt.Errorf("commit SCUM console logs: %w", err) } return len(normalized), nil } func (store *SCUMSQLStore) StoreSemanticEventBatch(ctx context.Context, batch SemanticEventBatch) (int, error) { if store == nil || store.db == nil { return 0, fmt.Errorf("SCUM plugin SQL store is not configured") } normalized := make([]SemanticEvent, 0, len(batch.Events)) for _, event := range batch.Events { value, err := normalizeSemanticEvent(event) if err != nil { return 0, err } normalized = append(normalized, value) } if len(normalized) == 0 { return 0, nil } tx, err := store.db.BeginTx(ctx, nil) if err != nil { return 0, fmt.Errorf("begin SCUM semantic event write: %w", err) } defer tx.Rollback() stamp := time.Now().UTC() for _, event := range normalized { if _, err := tx.ExecContext(ctx, scumSemanticEventInsertSQL, scumSemanticEventRecordKey(event), event.ServerID, event.Sequence, event.Type, event.PlayerID, nullText(event.DisplayName), event.OccurredAt, nullText(event.NetworkCorrelation), stamp, stamp); err != nil { return 0, fmt.Errorf("write SCUM semantic event: %w", err) } } if err := tx.Commit(); err != nil { return 0, fmt.Errorf("commit SCUM semantic events: %w", err) } return len(normalized), nil } func normalizeConsoleRecord(record ConsoleRecord) (ConsoleRecord, error) { record.ServerID = strings.TrimSpace(record.ServerID) record.Stream = strings.TrimSpace(record.Stream) record.Text = strings.TrimRight(record.Text, "\r\n") if record.ServerID == "" || (record.Stream != "stdout" && record.Stream != "stderr") || record.Sequence == 0 || record.OccurredAt.IsZero() || strings.TrimSpace(record.Text) == "" || len(record.Text) > 8192 { return ConsoleRecord{}, fmt.Errorf("SCUM console record is invalid") } record.OccurredAt = record.OccurredAt.UTC() return record, nil } func normalizeSemanticEvent(event SemanticEvent) (SemanticEvent, error) { event.ServerID = strings.TrimSpace(event.ServerID) event.Type = strings.TrimSpace(event.Type) event.PlayerID = strings.TrimSpace(event.PlayerID) event.DisplayName = strings.TrimSpace(event.DisplayName) event.NetworkCorrelation = strings.TrimSpace(event.NetworkCorrelation) if event.ServerID == "" || event.Sequence == 0 || event.Type == "" || event.PlayerID == "" || event.OccurredAt.IsZero() || len(event.Type) > 80 || len(event.PlayerID) > 80 || len(event.DisplayName) > 120 || len(event.NetworkCorrelation) > 128 { return SemanticEvent{}, fmt.Errorf("SCUM semantic event is invalid") } event.OccurredAt = event.OccurredAt.UTC() return event, nil } func scumConsoleRecordKey(record ConsoleRecord) string { digest := sha256.Sum256([]byte(strings.Join([]string{record.ServerID, record.Stream, fmt.Sprintf("%d", record.Sequence)}, "\x00"))) return hex.EncodeToString(digest[:]) } func scumSemanticEventRecordKey(event SemanticEvent) string { digest := sha256.Sum256([]byte(strings.Join([]string{event.ServerID, event.Type, event.PlayerID, fmt.Sprintf("%d", event.Sequence)}, "\x00"))) return hex.EncodeToString(digest[:]) } const scumConsoleLogInsertSQL = ` INSERT INTO scum_console_logs ( record_key, server_instance_id, stream, sequence, occurred_at, line_text, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE occurred_at = VALUES(occurred_at), line_text = VALUES(line_text), updated_at = VALUES(updated_at)` const scumSemanticEventInsertSQL = ` INSERT INTO scum_semantic_events ( record_key, server_instance_id, sequence, event_type, player_id, display_name, occurred_at, network_correlation, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE display_name = VALUES(display_name), occurred_at = VALUES(occurred_at), network_correlation = VALUES(network_correlation), updated_at = VALUES(updated_at)`