ProfileService
Overview
The ProfileService is a unified platform user enrichment system that combines database records with real-time platform API data. It provides a single get_profile() method that returns a UnifiedProfile for any platform user, transparently handling caching, enrichment staleness, and API failure protection via a per-account+platform circuit breaker.
Architecture
API / Chat Worker
|
v
ProfileService.get_profile(account_id, platform, platform_user_id, credentials?)
|
+-- 1. Redis cache check (lumio:user_profile:{account}:{platform}:{user})
| Hit? --> Return cached UnifiedProfile
|
+-- 2. Database lookup (platform_users table)
| Found? --> UnifiedProfile::from_db(row)
| Not found? --> UnifiedProfile::empty(platform, user_id)
|
+-- 3. Enrichment check (enriched_at > 24h ago or never?)
| |
| +-- Circuit breaker open? --> Cache with 15min TTL, return DB data
| |
| +-- No credentials? --> Cache with 30min TTL, return DB data
| |
| +-- Enrich via platform API
| |
| +-- Success --> Apply enrichment, persist to DB, cache with 4h TTL
| |
| +-- Failure --> Record failure (circuit breaker), cache with 30min TTL
|
v
UnifiedProfile
Multi-Platform Enrichment
Each platform has a dedicated enrichment method with different data coverage:
| Platform | Avatar | Bio/Description | Account Age | Broadcaster Type | Follower Status |
|---|---|---|---|---|---|
| Twitch | Yes | Yes | Yes | Yes | Yes (requires scope) |
| YouTube | Yes | Yes | Yes | -- | -- |
| Kick | Yes | Yes | -- | -- | -- |
| Trovo | Yes | Yes | Yes | -- | Yes |
Twitch provides the richest enrichment through the Helix API:
- User info: avatar, description, broadcaster type, account creation date
- Follower check: requires
platform_channel_idin credentials; non-fatal if scope is missing
YouTube uses the YouTube Data API to fetch channel snippet data (thumbnails, description, published date).
Kick and Trovo use their respective platform APIs for basic profile and channel information.
Caching Strategy
All profiles are cached in Redis under the key lumio:user_profile:{account_id}:{platform}:{platform_user_id}.
| Scenario | TTL | Description |
|---|---|---|
| Enrichment succeeded | 4 hours | Full profile with fresh API data |
| DB-only (no credentials or enrichment not needed) | 30 minutes | Shorter TTL to re-check sooner |
| Circuit breaker open | 15 minutes | Minimal TTL during API outage |
| Enrichment fresh (< 24h) | 4 hours | No re-enrichment needed |
Enrichment Staleness
Enrichment is triggered when enriched_at is either None (never enriched) or older than 24 hours. This ensures profiles stay reasonably fresh without hammering platform APIs on every request.
Circuit Breaker
The circuit breaker protects against cascading failures when a platform API is down or rate-limited. It operates per account+platform combination.
Configuration
| Parameter | Value | Description |
|---|---|---|
| Failure threshold | 5 | Failures within the window to trip the breaker |
| Failure window | 60 seconds | Rolling window for counting failures |
| Cooldown | 300 seconds (5 min) | How long the circuit stays open |
Mechanism
- Each enrichment failure increments a Redis counter (
lumio:circuit_count:{account}:{platform}). - The counter expires after the failure window (60s) if no further failures occur.
- When the counter reaches 5, a cooldown key (
lumio:circuit:{account}:{platform}) is set with a 5-minute TTL. - While the cooldown key exists,
is_circuit_openreturnstrueand enrichment is skipped. - After the cooldown expires, enrichment attempts resume.
UnifiedProfile
The UnifiedProfile struct combines database fields with enrichment data:
Database Fields
platform,platform_user_id,username,display_nameavatar_url,color(chat color)is_mod,is_sub,is_vip,badgesmessage_count,first_seen_at,last_seen_atis_banned,banned_at,banned_by,ban_reason,ban_type,timeout_expires_atuser_treatment,treatment_updated_at,treatment_updated_by
Enrichment Fields
description-- User bio from platform APIbroadcaster_type-- Twitch-specific (partner, affiliate, etc.)is_follower,followed_at-- Follower relationship to the channelaccount_created_at-- When the platform account was createdenriched_at-- Timestamp of last successful enrichment
Enrichment Merge
apply_enrichment() only overwrites fields that have Some values in the enrichment data, preserving existing database values for fields the platform does not provide.
Credentials
The ChannelCredentials struct is provided by the caller and contains decrypted OAuth tokens:
struct ChannelCredentials {
client_id: String, // Platform app client ID
client_secret: String, // Platform app client secret
access_token: String, // Channel's OAuth access token
refresh_token: Option<String>, // Channel's OAuth refresh token
platform_channel_id: Option<String>, // Broadcaster ID (for follower checks)
}
Credentials are loaded and decrypted by the API layer (apps/api) from the app_credentials and channel_connections tables, keeping the crypto module decoupled from lo-chat.
Key Files
| File | Purpose |
|---|---|
crates/lo-chat/src/profile_service.rs | ProfileService implementation, enrichment, circuit breaker |
crates/lo-chat/src/platform_users.rs | Database operations for platform_users table |
crates/lo-twitch-api/ | Twitch Helix API client (used for Twitch enrichment) |
crates/lo-youtube-api/ | YouTube Data API client (used for YouTube enrichment) |
crates/lo-kick-api/ | Kick API client (used for Kick enrichment) |
crates/lo-trovo-api/ | Trovo API client (used for Trovo enrichment) |