Skip to content

Instantly share code, notes, and snippets.

@AlcaDesign
Created June 16, 2025 08:48
Show Gist options
  • Save AlcaDesign/cb0b3ef4059a5d928b8a9a61a54b0fb7 to your computer and use it in GitHub Desktop.
Save AlcaDesign/cb0b3ef4059a5d928b8a9a61a54b0fb7 to your computer and use it in GitHub Desktop.
Parse Twitch emotes and cheermotes for (old) tmi.js
const prefixes = [
'Cheer', 'DoodleCheer', 'cheerwhal', 'Corgo', 'Scoops', 'uni', 'ShowLove', 'Party', 'SeemsGood', 'Pride',
'Kappa', 'FrankerZ', 'HeyGuys', 'DansGame', 'TriHard', 'Kreygasm', '4Head', 'SwiftRage', 'NotLikeThis',
'FailFish', 'VoHiYo', 'PJSalt', 'MrDestructoid', 'bday', 'RIPCheer', 'Shamrock', 'BitBoss', 'Streamlabs',
'Muxy', 'HolidayCheer', 'Goal', 'Anon', 'Charity'
];
const cheermoteRegex = new RegExp(`\\b(${prefixes.join('|')})(\\d+)\\b`, 'ig');
function onMessage(channel, tags, messageText, self) {
// Spread syntax or Array.from to preserve unicode surrogate pairs (emojis) to match Twitch's behavior
const textArr = [ ...messageText ];
const parts = [ messageText ];
if(tags.emotes) {
parts.splice(0);
/** @type {{ start: number; end: number; emoteId: string; ele: HTMLImageElement; }[]} */
const emotes = [];
for(const [ emoteId, indices ] of Object.entries(tags.emotes || {})) {
for(const n of indices) {
const spl = n.split('-');
const [ start, end ] = [ +spl[0], +spl[1] + 1 ];
const text = textArr.slice(start, end).join('');
const ele = renderEmote(emoteId, text);
emotes.push({ start, end, emoteId, ele });
}
}
emotes.sort((a, b) => a.start - b.start);
parts.push(textArr.slice(0, emotes[0].start).join(''));
for(let i = 0; i < emotes.length; i++) {
const { end, ele } = emotes[i];
const nextEmote = emotes[i + 1];
parts.push(ele, textArr.slice(end, nextEmote ? nextEmote.start : undefined).join(''));
}
}
// Check for cheermotes in the message
for(let i = parts.length - 1; i >= 0; i--) {
const n = parts[i];
if(typeof n !== 'string') {
continue;
}
/** @type {{ start: number; end: number; ele: HTMLSpanElement; }[]} */
const cheermotes = [];
for(const m of n.matchAll(cheermoteRegex)) {
const [ match, prefix, amountString ] = m;
if(amountString.startsWith('0')) {
// Skip cheermotes that start with 0, which are invalid
continue;
}
const amount = parseInt(amountString, 10);
const ele = renderCheermote(prefix, amount);
const [ start, end ] = [ m.index, m.index + match.length ];
cheermotes.push({ start, end, ele });
}
parts.splice(
i, 1,
n.slice(0, cheermotes[0]?.start ?? n.length),
...cheermotes.flatMap(({ end, ele: span }, i) => {
const nextCheer = cheermotes[i + 1];
return [ span, n.slice(end, nextCheer ? nextCheer.start : n.length) ];
})
);
}
return parts;
}
function renderCheermote(prefix, amt) {
const image = document.createElement('img');
image.className = 'chat-cheer';
const tier = amt >= 10000 ? 10000 : amt >= 5000 ? 5000 : amt >= 1000 ? 1000 : amt >= 100 ? 100 : 1;
image.src = `https://d3aqoihi2n8ty8.cloudfront.net/actions/${prefix.toLowerCase()}/dark/animated/${tier}/4.gif`;
image.alt = image.title = `${prefix}${amt}`;
const span = document.createElement('span');
span.className = 'chat-cheer-container';
span.append(image, ` ${amt}`);
return span;
}
function renderEmote(emoteId, text) {
const image = new Image();
image.className = 'chat-emote';
image.src = `https://static-cdn.jtvnw.net/emoticons/v2/${emoteId}/default/dark/3.0`;
image.alt = text;
return image;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment