Skip to main content

ProfileService

The ProfileService is a unified profile enrichment system that combines local database records with live platform API data. It provides a single entry point for fetching user profiles across all supported chat platforms.

Architecture

  • Crate: crates/lo-chat/src/profile_service.rs
  • Single endpoint: platformUserProfile GraphQL query
  • Platforms: Twitch (full), YouTube (partial), Kick (partial), Trovo (partial)

Core Struct

pub struct ProfileService {
pub redis: lo_cache::RedisClient,
pub db: sqlx::PgPool,
pub http: reqwest::Client,
}

The service is instantiated once at API startup and shared across all request handlers. It holds a Redis client for caching, a PostgreSQL connection pool for database access, and a shared HTTP client for platform API calls.

Data Flow

The get_profile() method follows a layered lookup strategy:

flowchart TD
A["get_profile(account_id, platform, user_id, credentials)"] --> B{Redis cache?}
B -->|Hit| C[Return cached profile]
B -->|Miss| D[Load from platform_users DB table]
D --> E{Enrichment stale?}
E -->|"Fresh (< 24h)"| F["Cache with 4h TTL, return"]
E -->|"Stale (> 24h)"| G{Credentials provided?}
G -->|No| H["Cache DB-only profile (30min TTL)"]
G -->|Yes| I{Circuit breaker open?}
I -->|Open| J["Cache DB profile (15min TTL)"]
I -->|Closed| K[Call platform API]
K -->|Success| L[Merge enrichment into profile]
L --> M[Persist to DB]
M --> N["Cache with 4h TTL"]
K -->|Failure| O[Record failure in circuit breaker]
O --> P["Cache DB profile (30min TTL)"]

style A fill:#3b82f6,color:#fff
style K fill:#f59e0b,color:#fff
style C fill:#10b981,color:#fff

Step-by-step

  1. Redis cache check -- Key: lumio:user_profile:{account_id}:{platform}:{user_id}. If found, return immediately.
  2. Database lookup -- Query the platform_users table for the user's stored profile data (message count, mod/sub/vip status, ban info, etc.).
  3. Enrichment check -- If the profile's enriched_at timestamp is older than 24 hours (or null), enrichment is needed.
  4. Circuit breaker check -- If the circuit is open for this account+platform, skip API calls and return DB data.
  5. Platform API call -- Fetch additional profile data from the platform API using decrypted credentials.
  6. Merge and persist -- Apply enrichment data to the profile, write enrichment fields back to the database.
  7. Cache -- Store the profile in Redis with the appropriate TTL.

Cache Keys and TTLs

Key PatternTTLCondition
lumio:user_profile:{account_id}:{platform}:{user_id}4 hours (14400s)Successful enrichment
lumio:user_profile:{account_id}:{platform}:{user_id}30 minutes (1800s)DB-only fallback (no credentials or enrichment failure)
lumio:user_profile:{account_id}:{platform}:{user_id}15 minutes (900s)Circuit breaker open

Circuit Breaker

The circuit breaker protects against cascading failures when a platform API is unavailable or rate-limited.

Keys

Key PatternPurpose
lumio:circuit:{account_id}:{platform}Cooldown flag -- existence means circuit is open
lumio:circuit_count:{account_id}:{platform}Failure counter within the current window

Parameters

ParameterValueDescription
Threshold5 failuresNumber of failures before the circuit opens
Window60 secondsTime window for counting failures
Cooldown300 seconds (5 min)Duration the circuit stays open

Behavior

  1. Each enrichment failure calls record_failure(), which increments lumio:circuit_count:{account_id}:{platform}.
  2. On the first failure, the counter gets a 60-second TTL (the window).
  3. When the counter reaches 5, the circuit opens:
    • A lumio:circuit:{account_id}:{platform} key is set with a 300-second TTL.
    • The counter is reset.
  4. While the circuit is open, all enrichment attempts for that account+platform are skipped.
  5. After the cooldown expires (key TTL), the circuit closes automatically.

Platform Enrichment Data

Each platform provides different enrichment fields:

Twitch (Full Enrichment)

FieldSource
avatar_urlprofile_image_url from Helix Users API
descriptionUser bio from Helix Users API
broadcaster_type"partner", "affiliate", or ""
account_created_atUser creation date
is_followerChecked via Helix Followers API
followed_atFollower timestamp (if following)

Requires moderator:read:followers scope for follower checks. If the scope is missing, the follower check fails gracefully (non-fatal).

YouTube (Avatar, Bio, Account Age)

FieldSource
avatar_urlChannel snippet thumbnails (high > medium > default)
descriptionChannel snippet description
account_created_atChannel publishedAt date

Kick (Avatar, Bio)

FieldSource
avatar_urlUser profile_picture
descriptionChannel channel_description

Two API calls: one for the user profile (avatar), one for the channel info (description).

Trovo (Avatar, Bio, Follower Status)

FieldSource
avatar_urlChannel profile_pic
descriptionChannel streamer_info
account_created_atChannel creation date
is_followerChecked via Trovo follower API
followed_atFollower timestamp (if following)

ChannelCredentials

The ChannelCredentials struct decouples the ProfileService from the encryption module:

pub struct ChannelCredentials {
pub client_id: String, // From app_credentials (decrypted)
pub client_secret: String, // From app_credentials (decrypted)
pub access_token: String, // From channel_connections (decrypted)
pub refresh_token: Option<String>, // From channel_connections (decrypted)
pub platform_channel_id: Option<String>, // Broadcaster ID
}

The API layer is responsible for loading credentials from the database, decrypting them, and constructing this struct. The ProfileService never touches encryption directly.

UnifiedProfile

The UnifiedProfile struct combines database fields with enrichment data:

FieldSourceDescription
platformDBPlatform identifier
platform_user_idDBPlatform-specific user ID
usernameDBPlatform username
display_nameDBDisplay name (may differ from username)
avatar_urlEnrichmentProfile picture URL
colorDBChat color
is_mod / is_sub / is_vipDBRole flags from chat messages
badgesDBPlatform badges (JSON)
message_countDBTotal messages seen
first_seen_at / last_seen_atDBActivity timestamps
is_bannedDBBan status
banned_at / banned_by / ban_reason / ban_typeDBBan details
timeout_expires_atDBTimeout expiration
user_treatmentDB"none", "suspicious", or "monitored"
descriptionEnrichmentUser bio
broadcaster_typeEnrichmentTwitch broadcaster type
is_follower / followed_atEnrichmentFollower status
account_created_atEnrichmentAccount age
enriched_atDBLast enrichment timestamp

Key Files

FilePurpose
crates/lo-chat/src/profile_service.rsProfileService, ChannelCredentials, UnifiedProfile, circuit breaker
crates/lo-chat/src/platform_users.rsDatabase queries for platform_users table
apps/api/src/graphql/chat.rsplatformUserProfile GraphQL resolver
apps/api/src/routes/chat.rsREST endpoint for profile lookup
crates/lo-twitch-api/Twitch Helix API client
crates/lo-youtube-api/YouTube Data API client
crates/lo-kick-api/Kick API client
crates/lo-trovo-api/Trovo Open Platform API client