Skip to main content

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:

PlatformAvatarBio/DescriptionAccount AgeBroadcaster TypeFollower Status
TwitchYesYesYesYesYes (requires scope)
YouTubeYesYesYes----
KickYesYes------
TrovoYesYesYes--Yes

Twitch provides the richest enrichment through the Helix API:

  • User info: avatar, description, broadcaster type, account creation date
  • Follower check: requires platform_channel_id in 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}.

ScenarioTTLDescription
Enrichment succeeded4 hoursFull profile with fresh API data
DB-only (no credentials or enrichment not needed)30 minutesShorter TTL to re-check sooner
Circuit breaker open15 minutesMinimal TTL during API outage
Enrichment fresh (< 24h)4 hoursNo 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

ParameterValueDescription
Failure threshold5Failures within the window to trip the breaker
Failure window60 secondsRolling window for counting failures
Cooldown300 seconds (5 min)How long the circuit stays open

Mechanism

  1. Each enrichment failure increments a Redis counter (lumio:circuit_count:{account}:{platform}).
  2. The counter expires after the failure window (60s) if no further failures occur.
  3. When the counter reaches 5, a cooldown key (lumio:circuit:{account}:{platform}) is set with a 5-minute TTL.
  4. While the cooldown key exists, is_circuit_open returns true and enrichment is skipped.
  5. 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_name
  • avatar_url, color (chat color)
  • is_mod, is_sub, is_vip, badges
  • message_count, first_seen_at, last_seen_at
  • is_banned, banned_at, banned_by, ban_reason, ban_type, timeout_expires_at
  • user_treatment, treatment_updated_at, treatment_updated_by

Enrichment Fields

  • description -- User bio from platform API
  • broadcaster_type -- Twitch-specific (partner, affiliate, etc.)
  • is_follower, followed_at -- Follower relationship to the channel
  • account_created_at -- When the platform account was created
  • enriched_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.

Manual Refresh

Users with the chat:refresh_user permission can manually trigger a profile re-enrichment via a Refresh button in the UserInfoModal. This bypasses the normal staleness interval and forces an immediate enrichment call against the platform API.

Rate Limiting

Manual refresh is rate-limited to once every 10 minutes per (account, platform, user) triple. The limit is enforced via a Redis SETNX cooldown key (lumio:profile_refresh_cooldown:{account_id}:{platform}:{platform_user_id}) with a 10-minute TTL. Attempting a refresh before the cooldown expires returns an error with REFRESH_COOLDOWN and the remaining seconds.

Platform Support

Manual refresh is available for all four supported platforms:

PlatformEnrichment triggered
TwitchFull enrichment (avatar, bio, account age, broadcaster type, follower status)
YouTubeAvatar, bio, account creation date
KickAvatar, bio
TrovoAvatar, bio, account creation date, follower status

Enrichment Intervals

Auto-enrichment intervals are configurable via the [profile] TOML section:

[profile]
# YouTube (days). 0 = disabled (manual refresh only). Default: 14.
youtube_enrichment_interval_days = 14
# Twitch, Kick, Trovo (days). 0 = disabled (manual refresh only). Default: 1.
enrichment_interval_days = 1

ENV overrides: LUMIO__PROFILE__YOUTUBE_ENRICHMENT_INTERVAL_DAYS, LUMIO__PROFILE__ENRICHMENT_INTERVAL_DAYS.

PlatformDefault intervalConfig key
Twitch1 dayenrichment_interval_days
Kick1 dayenrichment_interval_days
Trovo1 dayenrichment_interval_days
YouTube14 daysyoutube_enrichment_interval_days

Setting an interval to 0 disables automatic re-enrichment for that platform — profiles are only enriched on first view and via the manual refresh button. First-time enrichment (user never enriched before) always runs regardless of the interval setting.

YouTube's longer default interval reflects the YouTube Data API quota cost per enrichment call. Manual refresh still resets the enriched_at timestamp regardless of platform.

Key Files

FilePurpose
crates/lo-chat/src/profile_service.rsProfileService implementation, enrichment, circuit breaker
crates/lo-chat/src/platform_users.rsDatabase 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)