Emotes
Overview
The emotes module fetches, caches, and parses chat emotes from multiple providers across platforms. It supports both channel-specific emotes (fetched per platform + channel ID) and user-specific emotes (Twitch subscriber emotes for the authenticated user). Emotes are used in chat message rendering and overlay alerts.
Architecture
Dashboard UI / Overlay
|
v
Next.js API Proxy (/api/emotes)
|
v
REST API (GET /v1/emotes/{platform}/{channel_id}, GET /v1/emotes/user)
|
+--> Redis cache check
| Hit? --> Return cached EmoteSet[]
|
+--> Fetch from provider APIs (7TV, FFZ, BTTV, Twitch, etc.)
|
+--> Cache result in Redis
|
v
EmoteSet[] response
Emote Providers
| Provider | Enum Value | Platforms | Description |
|---|---|---|---|
| Twitch | twitch | Twitch | Native Twitch emotes (global + channel) |
| 7TV | 7tv | Twitch, YouTube, Kick | Third-party emote service |
| FrankerFaceZ | ffz | Twitch, YouTube | Third-party emote extension |
| BetterTTV | bttv | Twitch, YouTube, Kick | Third-party emote extension |
| Discord | discord | Discord | Guild custom emojis |
| YouTube | youtube | YouTube | YouTube native chat emojis (~100 standard shortcodes such as :yt:, :hand-pink-waving:) |
| Kick | kick | Kick | Kick platform emotes |
| Trovo | trovo | Trovo | Trovo platform emotes |
Per-Platform Fetch Strategy
| Platform | Providers Fetched |
|---|---|
| Twitch | 7TV (channel), FFZ (channel), BTTV (channel), FFZ (global) |
| YouTube | YouTube (native standard emojis), 7TV (channel), BTTV (channel), FFZ (channel + global) |
| Kick | 7TV (channel), BTTV (channel), FFZ (global) |
| Trovo | Trovo (native, requires client_id), 7TV (global), BTTV (global), FFZ (global) |
| Discord | Discord guild emojis (requires bot token) |
Empty sets are filtered out -- only providers with at least one emote are included in the response.
Provider Channel-Lookup URLs
| Provider | Twitch | YouTube |
|---|---|---|
| 7TV | https://7tv.io/v3/users/twitch/{channel_id} | https://7tv.io/v3/users/youtube/{channel_id} |
| BTTV | https://api.betterttv.net/3/cached/users/twitch/{channel_id} | https://api.betterttv.net/3/cached/users/youtube/{channel_id} |
| FFZ | https://api.frankerfacez.com/v1/room/id/{channel_id} | https://api.frankerfacez.com/v1/room/yt/{channel_id} |
Note on FFZ: while 7TV/BTTV use a /users/... URL pattern, FFZ uses /room/.... Both are equivalent in semantics — they return the channel-active emote set. FFZ's /user/... endpoint returns user-profile data (uploaded sets, badges) and is not what the chat renders.
API
REST Endpoints
| Method | Path | Description | Auth |
|---|---|---|---|
GET | /v1/emotes/{platform}/{channel_id} | Fetch channel emotes | Authenticated |
GET | /v1/emotes/user | Fetch user's Twitch subscriber emotes | Authenticated |
GET /v1/emotes/{platform}/{channel_id}
Fetches all available emote sets for a given platform and channel. Results are cached in Redis.
Response:
{
"data": {
"sets": [
{
"provider": "7tv",
"emotes": [
{
"id": "60ae958e...",
"name": "LULW",
"url": "https://cdn.7tv.app/emote/.../1x.webp",
"provider": "7tv",
"animated": false
}
]
}
],
"owners": null
}
}
GET /v1/emotes/user
Fetches all Twitch emotes available to the authenticated user (subscriber emotes from all channels). Requires a Twitch login connection with user:read:emotes scope. Automatically refreshes the token if expired.
Also resolves emote owner information (channel names and avatars) via batch Twitch user lookup.
Response includes:
sets-- Array ofEmoteSetwith the user's available emotesowners-- Map of owner IDs to{ displayName, profileImageUrl }for grouping emotes by channel
Types
EmoteSet
struct EmoteSet {
provider: EmoteProvider, // twitch, 7tv, ffz, bttv, discord, youtube, kick, trovo
emotes: Vec<Emote>,
}
Emote
struct Emote {
id: String, // Provider-specific emote ID
name: String, // Emote name (e.g., "Kappa", "LULW")
url: String, // CDN URL for the emote image
provider: EmoteProvider, // Source provider
animated: bool, // Whether the emote is animated (GIF/WEBP)
owner_id: Option<String>, // Owner user/channel ID (Twitch-specific)
}
Message Parsing
The parse_emotes() function splits a chat message into segments of text and emotes:
fn parse_emotes(message: &str, emote_sets: &[EmoteSet]) -> Vec<EmotePart>
Behavior:
- Builds a name-to-emote lookup from all provided sets
- Splits message on whitespace boundaries
- Exact, case-sensitive matching only (
Kappamatches,kappadoes not,KappaRossdoes not) - Consecutive text words are merged into a single
EmotePart::Text - Returns
Vec<EmotePart>where each part is eitherText { text }orEmote { id, name, url, provider }
YouTube Standard Emojis
YouTube Live Chat ships with ~100 platform-native standard emojis (e.g. :yt:, :hand-pink-waving:, :dothefive:). YouTube does not expose an API for the catalogue, so the list is curated manually in crates/lo-chat/src/youtube_emotes.rs and embedded in the REST/GraphQL response for platform = "youtube" as an additional EmoteSet with provider = "youtube". The set is pushed first in the response so the frontend's first-write-wins emote map gives YouTube native emojis priority over any 7TV/BTTV/FFZ entry that happens to share a shortcode.
Sending: YouTube Live Chat renders :shortcode: tokens server-side, so chat messages sent through Lumio with YouTube shortcodes are forwarded as plain text in messageText and YouTube turns them into emoji images on delivery. No client-side conversion is required.
Rendering: Incoming YouTube messages keep the raw shortcode in message_text. The MessageContent renderer (apps/web/src/app/(app)/dashboard/chat/message-content.tsx) and the lighter EmoteText component split each whitespace-token by the shortcode regex :[a-zA-Z0-9_-]+: and replace runs of back-to-back shortcodes (e.g. :hand-pink-waving::hand-pink-waving:) with separate <img> elements separated by visible spaces.
Adding new emojis: Append a (shortcode, cdn_url) tuple to YOUTUBE_STANDARD_EMOTES in crates/lo-chat/src/youtube_emotes.rs and rebuild the API binary.
Caching
Emote sets are cached in Redis per platform + channel ID. The cache is checked before making any external API calls. Cache keys follow the pattern used by emote_service::get_cached_emotes / cache_emotes.
User emotes are cached separately under twitch-user:user:{user_id} with owner data cached under lumio:emotes:owners:{user_id} (1 hour TTL).
Key Files
| File | Purpose |
|---|---|
apps/api/src/routes/emotes.rs | REST endpoints for channel and user emotes |
apps/api/src/services/emotes.rs | Provider fetch functions, caching, owner resolution |
crates/lo-chat/src/emotes.rs | Core types (Emote, EmoteSet, EmoteProvider, EmotePart) and parse_emotes() |
crates/lo-chat/src/youtube_emotes.rs | Static catalogue of YouTube native standard-emoji shortcodes |
apps/web/src/app/(app)/dashboard/chat/message-content.tsx | Multichat renderer with shortcode-splitting + colon-fallback |
apps/web/src/app/(app)/dashboard/chat/emote-library.tsx | Emote picker with platform tabs and the youtube-global section |