package cpnotice import ( "encoding/json" "fmt" "strconv" "strings" ) // cpApplication mirrors the user-service CP outbox payload without importing user-service internals. type cpApplication struct { ApplicationID string RelationType string Status string Requester cpUser Target cpUser RoomID string RoomRegionID int64 Gift cpGift ExpiresAtMS int64 DecidedAtMS int64 } type cpUser struct { UserID int64 DisplayUserID string Username string Avatar string } type cpGift struct { GiftID string GiftName string GiftIconURL string GiftAnimationURL string GiftCount int64 GiftValue int64 BillingReceiptID string } func decodeObject(value string) (map[string]any, error) { var payload map[string]any if err := json.Unmarshal([]byte(strings.TrimSpace(value)), &payload); err != nil { return nil, fmt.Errorf("invalid cp payload json: %w", err) } if payload == nil { payload = map[string]any{} } return payload, nil } func applicationFromObject(obj map[string]any) (cpApplication, error) { if len(obj) == 0 { return cpApplication{}, fmt.Errorf("cp application payload is missing") } app := cpApplication{ ApplicationID: stringValue(obj, "application_id", "ApplicationID"), RelationType: stringValue(obj, "relation_type", "RelationType"), Status: stringValue(obj, "status", "Status"), Requester: userFromObject(firstObject(obj, "requester", "Requester")), Target: userFromObject(firstObject(obj, "target", "Target")), RoomID: stringValue(obj, "room_id", "RoomID"), RoomRegionID: int64Value(obj, "room_region_id", "RoomRegionID"), Gift: giftFromObject(firstObject(obj, "gift", "Gift")), ExpiresAtMS: int64Value(obj, "expires_at_ms", "ExpiresAtMS"), DecidedAtMS: int64Value(obj, "decided_at_ms", "DecidedAtMS"), } if app.ApplicationID == "" || app.RelationType == "" || app.Requester.UserID <= 0 || app.Target.UserID <= 0 { return cpApplication{}, fmt.Errorf("cp application payload is incomplete") } return app, nil } func userFromObject(obj map[string]any) cpUser { return cpUser{ UserID: int64Value(obj, "user_id", "UserID"), DisplayUserID: stringValue(obj, "display_user_id", "DisplayUserID"), Username: stringValue(obj, "username", "Username"), Avatar: stringValue(obj, "avatar", "Avatar"), } } func giftFromObject(obj map[string]any) cpGift { return cpGift{ GiftID: stringValue(obj, "gift_id", "GiftID"), GiftName: stringValue(obj, "gift_name", "GiftName"), GiftIconURL: stringValue(obj, "gift_icon_url", "GiftIconURL"), GiftAnimationURL: stringValue(obj, "gift_animation_url", "GiftAnimationURL"), GiftCount: int64Value(obj, "gift_count", "GiftCount"), GiftValue: int64Value(obj, "gift_value", "GiftValue"), BillingReceiptID: stringValue(obj, "billing_receipt_id", "BillingReceiptID"), } } func firstObject(obj map[string]any, keys ...string) map[string]any { for _, key := range keys { if value, ok := obj[key]; ok { if typed, ok := value.(map[string]any); ok { return typed } // json.Unmarshal into map can leave nested structs only as maps; this defensive re-marshal keeps tests and // future typed adapters from breaking the parser if they pass a struct-like object. body, err := json.Marshal(value) if err == nil { var nested map[string]any if json.Unmarshal(body, &nested) == nil { return nested } } } } return map[string]any{} } func stringValue(obj map[string]any, keys ...string) string { for _, key := range keys { switch value := obj[key].(type) { case string: return strings.TrimSpace(value) case json.Number: return value.String() case float64: if value == float64(int64(value)) { return strconv.FormatInt(int64(value), 10) } case int64: return strconv.FormatInt(value, 10) case int: return strconv.Itoa(value) } } return "" } func int64Value(obj map[string]any, keys ...string) int64 { for _, key := range keys { switch value := obj[key].(type) { case float64: return int64(value) case int64: return value case int: return int64(value) case json.Number: parsed, _ := value.Int64() return parsed case string: parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64) return parsed } } return 0 }