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:
platformUserProfileGraphQL 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
- Redis cache check -- Key:
lumio:user_profile:{account_id}:{platform}:{user_id}. If found, return immediately. - Database lookup -- Query the
platform_userstable for the user's stored profile data (message count, mod/sub/vip status, ban info, etc.). - Enrichment check -- If the profile's
enriched_attimestamp is older than 24 hours (or null), enrichment is needed. - Circuit breaker check -- If the circuit is open for this account+platform, skip API calls and return DB data.
- Platform API call -- Fetch additional profile data from the platform API using decrypted credentials.
- Merge and persist -- Apply enrichment data to the profile, write enrichment fields back to the database.
- Cache -- Store the profile in Redis with the appropriate TTL.
Cache Keys and TTLs
| Key Pattern | TTL | Condition |
|---|---|---|
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 Pattern | Purpose |
|---|---|
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
| Parameter | Value | Description |
|---|---|---|
| Threshold | 5 failures | Number of failures before the circuit opens |
| Window | 60 seconds | Time window for counting failures |
| Cooldown | 300 seconds (5 min) | Duration the circuit stays open |
Behavior
- Each enrichment failure calls
record_failure(), which incrementslumio:circuit_count:{account_id}:{platform}. - On the first failure, the counter gets a 60-second TTL (the window).
- 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.
- A
- While the circuit is open, all enrichment attempts for that account+platform are skipped.
- After the cooldown expires (key TTL), the circuit closes automatically.
Platform Enrichment Data
Each platform provides different enrichment fields:
Twitch (Full Enrichment)
| Field | Source |
|---|---|
avatar_url | profile_image_url from Helix Users API |
description | User bio from Helix Users API |
broadcaster_type | "partner", "affiliate", or "" |
account_created_at | User creation date |
is_follower | Checked via Helix Followers API |
followed_at | Follower 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)
| Field | Source |
|---|---|
avatar_url | Channel snippet thumbnails (high > medium > default) |
description | Channel snippet description |
account_created_at | Channel publishedAt date |
Kick (Avatar, Bio)
| Field | Source |
|---|---|
avatar_url | User profile_picture |
description | Channel channel_description |
Two API calls: one for the user profile (avatar), one for the channel info (description).
Trovo (Avatar, Bio, Follower Status)
| Field | Source |
|---|---|
avatar_url | Channel profile_pic |
description | Channel streamer_info |
account_created_at | Channel creation date |
is_follower | Checked via Trovo follower API |
followed_at | Follower 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:
| Field | Source | Description |
|---|---|---|
platform | DB | Platform identifier |
platform_user_id | DB | Platform-specific user ID |
username | DB | Platform username |
display_name | DB | Display name (may differ from username) |
avatar_url | Enrichment | Profile picture URL |
color | DB | Chat color |
is_mod / is_sub / is_vip | DB | Role flags from chat messages |
badges | DB | Platform badges (JSON) |
message_count | DB | Total messages seen |
first_seen_at / last_seen_at | DB | Activity timestamps |
is_banned | DB | Ban status |
banned_at / banned_by / ban_reason / ban_type | DB | Ban details |
timeout_expires_at | DB | Timeout expiration |
user_treatment | DB | "none", "suspicious", or "monitored" |
description | Enrichment | User bio |
broadcaster_type | Enrichment | Twitch broadcaster type |
is_follower / followed_at | Enrichment | Follower status |
account_created_at | Enrichment | Account age |
enriched_at | DB | Last enrichment timestamp |
Key Files
| File | Purpose |
|---|---|
crates/lo-chat/src/profile_service.rs | ProfileService, ChannelCredentials, UnifiedProfile, circuit breaker |
crates/lo-chat/src/platform_users.rs | Database queries for platform_users table |
apps/api/src/graphql/chat.rs | platformUserProfile GraphQL resolver |
apps/api/src/routes/chat.rs | REST 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 |