#Custom HTML JS chat module

1 messages · Page 1 of 1 (latest)

oblique fiber
#

Im buildin a multichat widget with HTML, JS to put on obs as a web source with local file.
How amd I able to get and render 7tv emojis on the widget. I'm using streamer.bot as the chat source with web sockets, the JS subcribe to different types of events and manipualtes the DOM but some amojis are painted and others are just not rendered correctly or display incorrect image.

Im having some issues like founding 0 channel emotes form the streamer.

#

Didn't found documentation by myself but claude AI gave me this that partially work since it displays some global emotes but cant find channel emotes or it cant process all global emotes correctly

async function fetch7TVGlobal() {
    try {
        const res    = await fetch('https://7tv.io/v3/emote-sets/global');
        const data   = await res.json();
        const emotes = data.emotes ?? [];
        for (const e of emotes) {
            const host = e.data?.host;
            if (!host) continue;
            emoteMap.set(e.name, { url: `https:${host.url}/1x.webp`, provider: '7TV' });
        }
        console.log(`[7TV] ${emotes.length} global emotes loaded`);
    } catch (err) { console.warn('[7TV] Global fetch failed:', err); }
}

async function fetch7TVChannel(userId) {
    try {
        const res    = await fetch(`https://7tv.io/v3/users/twitch/${userId}`);
        const data   = await res.json();
        const emotes = data.emote_set?.emotes ?? [];
        for (const e of emotes) {
            const host = e.data?.host;
            if (!host) continue;
            emoteMap.set(e.name, { url: `https:${host.url}/1x.webp`, provider: '7TV' });
        }
        console.log(`[7TV] ${emotes.length} channel emotes loaded`);
    } catch (err) { console.warn('[7TV] Channel fetch failed:', err); }
}
#


function renderMessageText(text, twitchEmotes = []) {
    if (!text) return '';
    if (!CONFIG.emotes.enabled) return escapeHTML(text);

    // ── Pass 1: carve out Twitch native emote regions by index ───────────────
    const native = twitchEmotes
        .filter(e => typeof e.startIndex === 'number' && e.imageUrl)
        .sort((a, b) => a.startIndex - b.startIndex);

    const segments = []; // { type: 'text'|'emote', content?, name?, url? }
    let cursor = 0;

    for (const emote of native) {
        if (emote.startIndex > cursor) {
            segments.push({ type: 'text', content: text.slice(cursor, emote.startIndex) });
        }
        segments.push({ type: 'emote', name: emote.name, url: emote.imageUrl });
        cursor = emote.endIndex + 1;
    }
    if (cursor < text.length) segments.push({ type: 'text', content: text.slice(cursor) });
    if (segments.length === 0) segments.push({ type: 'text', content: text });

    // ── Pass 2: render each segment ──────────────────────────────────────────
    return segments.map(seg => {
        if (seg.type === 'emote') {
            return `<img class="chat-emote" src="${seg.url}" alt="${escapeHTML(seg.name)}" title="${escapeHTML(seg.name)}">`;
        }
        // Text segment: word-by-word BTTV/FFZ/7TV lookup
        return seg.content.split(' ').map(word => {
            if (!word) return '';
            const entry = emoteMap.get(word);
            if (entry) {
                return `<img class="chat-emote" src="${escapeHTML(entry.url)}" alt="${escapeHTML(word)}" title="${escapeHTML(word)} [${entry.provider}]">`;
            }
            return escapeHTML(word);
        }).join(' ');
    }).join('');
}
last knot
#

I reccomend using the gql instead to fetch emotes for this to be faster and efficient. https://7tv.io/v4/gql

oblique fiber
#

do you have any doc for the gql?

last knot
#

there is a link for a playground for it that shows the stuff for the quires

#

One second I can find it

oblique fiber
#

I appreciate that. ❤️

last knot
oblique fiber
#

I'll try to play whit this and understand it, thank you!

last knot
#

your welcome LacyLove

oblique fiber
#

Just for anyone interrested on this this is what playing got me and some research with cladue (TBH never used gql so was difficult)

/** Map CONFIG platform key → 7TV v4 GQL Platform enum value. */
const _7TV_PLATFORM_MAP = {
    twitchUserId:  'TWITCH',
    kickUserId:    'KICK',
    youtubeUserId: 'GOOGLE',    // 7TV uses GOOGLE for YouTube
};

/**
 * Build & execute a single GraphQL query that fetches:
 *   • global 7TV emotes
 *   • channel emotes for every configured platform ID
 * Returns { global: [...], channels: [...] }  (flat arrays of {code, url}).
 */
async function _fetch7TV() {
    const cfg = CONFIG.emotes;

    const connections = [];
    for (const [cfgKey, gqlPlatform] of Object.entries(_7TV_PLATFORM_MAP)) {
        const id = cfg[cfgKey];
        if (id && gqlPlatform) connections.push({ cfgKey, gqlPlatform, id });
    }

    // Requesting global plus user emotes in one query to save round-trips.
    const channelFragments = connections.map((c, i) =>
        `u${i}: userByConnection(platform: ${c.gqlPlatform}, platformId: "${c.id}") { emoteSets { emotes(perPage: 500) { items { alias emote { id } } } } }`
    ).join('\n        ');

    const query = `{
        emoteSets {
            global {
                emotes(perPage: 200) {
                    items { alias emote { id } }
                }
            }
        }
        ${connections.length ? `users {\n        ${channelFragments}\n    }` : ''}
    }`;

    const res  = await fetch('https://7tv.io/v4/gql', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query }),
    });
    const json = await res.json();
    if (json.errors?.length) console.warn('[7TV] GQL partial errors:', json.errors);

#
   // Global emtoes
    const globalItems = json.data?.emoteSets?.global?.emotes?.items ?? [];
    const globalOut = globalItems.map(e => ({
        code: e.alias,
        url:  `https://cdn.7tv.app/emote/${e.emote.id}/1x.webp`,
    }));

    // Channes with de duplication
    const seen = new Set(globalOut.map(e => e.code));
    const channelOut = [];
    for (let i = 0; i < connections.length; i++) {
        const user = json.data?.users?.[`u${i}`];
        if (!user) continue;
        for (const set of user.emoteSets ?? []) {
            for (const e of set.emotes?.items ?? []) {
                if (seen.has(e.alias)) continue;
                seen.add(e.alias);
                channelOut.push({
                    code: e.alias,
                    url:  `https://cdn.7tv.app/emote/${e.emote.id}/1x.webp`,
                });
            }
        }
    }

    return { global: globalOut, channels: channelOut };
}

Code was too long so I split it on two messages

last knot
#

do you need any help with the gql? as you said it was so difficult

oblique fiber
#

I think for now its good and we can mark this as closes I just posted the code for others users to have a solution.

Really... thank you! I need to study gql by my own. Thank you very much!