Chatter List Providers
The chatter list knows no platform. Every platform is a provider that implements ChatterProvider (apps/api/src/services/chatters/provider.rs) and is registered in the ProviderRegistry built by ChattersService::build (apps/api/src/services/chatters/mod.rs). TwitchHelixProvider (services/chatters/twitch.rs) is the reference implementation.
What a provider does not do: it never writes Redis state, never publishes deltas and never decides who is in or out. The poller (apps/api/src/workers/chatters.rs) and the pure state machine (services/chatters/model.rs) do that for every platform alike. People who write in the chat are fed in automatically from the normalised chat:message stream (lumio:chat:\{account_id\}, platform + user_id + badges), so a platform whose chat already reaches that stream gets its writers listed with no extra work.
The trait
| Method | Contract |
|---|---|
platform() | The platform slug, e.g. twitch. It becomes the platform field of the API contract. |
is_available(account) | Connection + scope check, DB only (it runs on the GraphQL/REST/bootstrap read path). Returns Available, MissingScope or NotConnected. |
snapshot(account, max) | The full chatter list (at most max), the platform's total, and the role inputs read with the same token. Ok(None) when the platform has no list endpoint — presence then comes from chat messages alone. Return ProviderError::MissingScope when the platform rejects the grant. |
role_of(badges) | Map one chat message's badges JSON onto broadcaster | moderator | bot | vip (None = viewer). |
known_bots() | Curated, lower-cased bot logins for this platform. |
base_role_inputs(account) | Broadcaster id + known bots (incl. the account's own bot account) without a platform call. |
avatars(account, ids) | Optional. user_id → url; cached by the poller under lumio:avatar:\{platform\}:\{uid\} for 24 h. |
Tokens come only from crate::oauth::get_fresh_connection_token on the account's channel connection — never the login token, never an inline refresh.
Add a platform — 6 steps
- Write the provider. Implement
ChatterProviderinapps/api/src/services/chatters/<platform>.rsand add it to the registry inChattersService::build. Add a[chatters.<platform>]table (enabled,poll_interval_secs,max_chatters) toChattersSettings::platformandconfig/default.toml; without one the provider runs on the defaults. Keep every platform-specific name inside the provider: the contract (Chatter,ChatterPlatformStatus,ChattersSnapshot) must not grow a platform field. - Check the scopes. List what the list/role endpoints need and add the scopes to the platform's channel scopes in
apps/api/src/platforms.rsand the frontend copyapps/web/src/app/(main)/(app)/dashboard/connections/credentials/platform-config.ts. A new scope makesmissing_scopes()ask every existing connection to reconnect once — bundle all scopes into one change so there is exactly one reconnect prompt. Makeis_availablereturnMissingScopefor a grant without them, and treat an unknown (NULL) stored grant as "try and see". - Map badges to roles. Implement
role_offor the platform's badge format. Identity (broadcaster, known bot) beats lists, lists beat badges, and bot beats moderator/VIP. Add unit tests next to the provider likeservices/chatters/twitch.rs. - Curate the bot list. Return the platform's well-known bots from
known_bots()(lower-case logins) and include Lumio's own bot account for that platform plus the account's custom bot (bot_connections.bot_username). - Wire the live signal. Live polling (and clearing the list at stream end) reads the account's online set
lumio:channel_online:\{account_id\}. The platform's go-live / go-offline path must callservices::channel_status::set_online/set_offlinewith the provider's platform string; Twitch, Kick and YouTube already do. Without it the platform is polled only while a panel is open, and its list is never cleared at stream end (it still expires 5 minutes after the last poll). - Document it. Add the platform to the roles table and the availability notes in Chatter List, its scopes to Channel Connections OAuth Scopes, and its config table to Installation.
The frontend needs no change: it renders whatever supported_platforms lists, and shows platform chips and per-row icons once two or more platforms are connected.