diff --git a/src/bttv_api.ts b/src/bttv_api.ts index 764451f..0ee9133 100644 --- a/src/bttv_api.ts +++ b/src/bttv_api.ts @@ -1,3 +1,6 @@ +/** + * An "Emote" as returned by the BTTV API + */ type BTTV_Emote = { id: string; code: string; @@ -6,21 +9,34 @@ type BTTV_Emote = { user: any; }; +/** + * Query BTTV API for an emote graphic + * + * @param code string associated with the emote graphic (often called "emote" itself) + * @returns `null` iff no emote found or error, + * else `[code, url]` tuple where `url` is the URL to a BTTV emote graphic + */ export async function bttv_get_url( code: string, ): Promise<[string, string] | null> { + // search for emotes const res = await fetch( `https://api.betterttv.net/3/emotes/shared/search?query=${code}&offset=0&limit=50`, ); const content = await res.json(); + // sanity check if (!Array.isArray(content)) return null; + + // find exact matches (disregard case) const matches = (content as BTTV_Emote[]).filter( (be) => be.code.toLowerCase() == code.toLowerCase(), ); + // ensure there's emotes left if (matches.length <= 0) return null; + // return first emote's URL return [ code, `//cdn.betterttv.net/emote/${matches[0].id}/1x.${matches[0].imageType}`, diff --git a/src/helpers.ts b/src/helpers.ts index 04ba5f2..7c8203b 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,11 +1,23 @@ +/** + * Async function mapping some `input` to an `[input, output]` tuple or a `null` if no output exists + */ type MapFunction = (input: In) => Promise<[In, Out] | null>; -export async function map_parallel( +/** + * Concurrently create some mapping of input to output elements. + * + * @param inputs collection of input elements + * @param map_fn function mapping inputs to outputs + * @returns ES6 Map object with inputs mapped to outputs where possible + */ +export async function map_concurrent( inputs: Iterable, map_fn: MapFunction, ): Promise> { + // apply map_fn to inputs const queue = [...inputs].map((input) => map_fn(input)); + // wait for calls to finish return new Map( (await Promise.all(queue)).filter((job): job is [In, Out] => job !== null), ); diff --git a/src/main.ts b/src/main.ts index d8543b3..7d68f95 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,44 +1,62 @@ import { bttv_get_url } from "./bttv_api"; -import { map_parallel } from "./helpers"; +import { map_concurrent } from "./helpers"; const emote_regex = /bttv:([^\s]+)/gi; +/** + * Process "bttv:*" emotes in a string + * + * @param text the input string + * @returns new string with existing "bttv:*" emotes replaced by "[not]" BBCodes + */ async function process_text(text: string): Promise { + // find "bttv:*" emotes const matches = [...text.matchAll(emote_regex)]; - const codes = new Set(matches.map(([_, code]) => code)); - console.log("[lmlfc-bttv]", `searching ${[...codes].length} emotes`); - const emotes = await map_parallel(codes, bttv_get_url); + // unique emote codes (the "*" parts) + const codes = new Set(matches.map(([_, code]) => code)); + + // find emote pics using BTTV API + console.log("[lmlfc-bttv]", `searching ${[...codes].length} emotes`); + const emotes = await map_concurrent(codes, bttv_get_url); + + // replace emotes with BBCodes for (const emote of matches.reverse()) { const [match, code] = emote; - if (emote.index == undefined) continue; - const index = emote.index; - if (!emotes.has(code)) continue; + // emote exists const url = emotes.get(code); + if (emote.index == undefined) continue; + // position in input string is known + const index = emote.index; + text = text.substring(0, index) + `[not]${url}[/not]` + text.substring(index + match.length); } + // remove space between adjacent emotes + // (maybe do this in emote_regex instead?) return text.replace(/\[\/not\]\s+\[not\]/g, "[/not][not]"); } (() => { + // find chatbox elements const cb_form = document.querySelector("#mgc_cb_evo_form"); - if (!(cb_form instanceof HTMLFormElement)) return; - const cb_send = document.querySelector( "#mgc_cb_evo_form > input[type=image]:nth-child(2)", ); - if (!(cb_send instanceof HTMLInputElement)) return; - const cb_input = document.querySelector("#mgc_cb_evo_input"); + + // check chatbox elements + if (!(cb_form instanceof HTMLFormElement)) return; + if (!(cb_send instanceof HTMLInputElement)) return; if (!(cb_input instanceof HTMLInputElement)) return; + // create "bttv" button const bttv_btn = (() => { const btn = document.createElement("a"); btn.style.setProperty("cursor", "pointer"); @@ -50,6 +68,9 @@ async function process_text(text: string): Promise { img.setAttribute("title", "bttv:* emotes ersetzen und absenden"); img.style.setProperty("vertical-align", "middle"); + // ****************** + // * business logic * + // ****************** btn.addEventListener("click", async () => { cb_input.value = await process_text(cb_input.value); cb_input.focus();