doc
This commit is contained in:
parent
795a481aaa
commit
ffb5a3b497
3 changed files with 61 additions and 12 deletions
|
@ -1,3 +1,6 @@
|
||||||
|
/**
|
||||||
|
* An "Emote" as returned by the BTTV API
|
||||||
|
*/
|
||||||
type BTTV_Emote = {
|
type BTTV_Emote = {
|
||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
|
@ -6,21 +9,34 @@ type BTTV_Emote = {
|
||||||
user: any;
|
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(
|
export async function bttv_get_url(
|
||||||
code: string,
|
code: string,
|
||||||
): Promise<[string, string] | null> {
|
): Promise<[string, string] | null> {
|
||||||
|
// search for emotes
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://api.betterttv.net/3/emotes/shared/search?query=${code}&offset=0&limit=50`,
|
`https://api.betterttv.net/3/emotes/shared/search?query=${code}&offset=0&limit=50`,
|
||||||
);
|
);
|
||||||
const content = await res.json();
|
const content = await res.json();
|
||||||
|
|
||||||
|
// sanity check
|
||||||
if (!Array.isArray(content)) return null;
|
if (!Array.isArray(content)) return null;
|
||||||
|
|
||||||
|
// find exact matches (disregard case)
|
||||||
const matches = (content as BTTV_Emote[]).filter(
|
const matches = (content as BTTV_Emote[]).filter(
|
||||||
(be) => be.code.toLowerCase() == code.toLowerCase(),
|
(be) => be.code.toLowerCase() == code.toLowerCase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ensure there's emotes left
|
||||||
if (matches.length <= 0) return null;
|
if (matches.length <= 0) return null;
|
||||||
|
|
||||||
|
// return first emote's URL
|
||||||
return [
|
return [
|
||||||
code,
|
code,
|
||||||
`//cdn.betterttv.net/emote/${matches[0].id}/1x.${matches[0].imageType}`,
|
`//cdn.betterttv.net/emote/${matches[0].id}/1x.${matches[0].imageType}`,
|
||||||
|
|
|
@ -1,11 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Async function mapping some `input` to an `[input, output]` tuple or a `null` if no output exists
|
||||||
|
*/
|
||||||
type MapFunction<In, Out> = (input: In) => Promise<[In, Out] | null>;
|
type MapFunction<In, Out> = (input: In) => Promise<[In, Out] | null>;
|
||||||
|
|
||||||
export async function map_parallel<In, Out>(
|
/**
|
||||||
|
* 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<In, Out>(
|
||||||
inputs: Iterable<In>,
|
inputs: Iterable<In>,
|
||||||
map_fn: MapFunction<In, Out>,
|
map_fn: MapFunction<In, Out>,
|
||||||
): Promise<Map<In, Out>> {
|
): Promise<Map<In, Out>> {
|
||||||
|
// apply map_fn to inputs
|
||||||
const queue = [...inputs].map((input) => map_fn(input));
|
const queue = [...inputs].map((input) => map_fn(input));
|
||||||
|
|
||||||
|
// wait for calls to finish
|
||||||
return new Map<In, Out>(
|
return new Map<In, Out>(
|
||||||
(await Promise.all(queue)).filter((job): job is [In, Out] => job !== null),
|
(await Promise.all(queue)).filter((job): job is [In, Out] => job !== null),
|
||||||
);
|
);
|
||||||
|
|
43
src/main.ts
43
src/main.ts
|
@ -1,44 +1,62 @@
|
||||||
import { bttv_get_url } from "./bttv_api";
|
import { bttv_get_url } from "./bttv_api";
|
||||||
import { map_parallel } from "./helpers";
|
import { map_concurrent } from "./helpers";
|
||||||
|
|
||||||
const emote_regex = /bttv:([^\s]+)/gi;
|
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<string> {
|
async function process_text(text: string): Promise<string> {
|
||||||
|
// find "bttv:*" emotes
|
||||||
const matches = [...text.matchAll(emote_regex)];
|
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()) {
|
for (const emote of matches.reverse()) {
|
||||||
const [match, code] = emote;
|
const [match, code] = emote;
|
||||||
|
|
||||||
if (emote.index == undefined) continue;
|
|
||||||
const index = emote.index;
|
|
||||||
|
|
||||||
if (!emotes.has(code)) continue;
|
if (!emotes.has(code)) continue;
|
||||||
|
// emote exists
|
||||||
const url = emotes.get(code);
|
const url = emotes.get(code);
|
||||||
|
|
||||||
|
if (emote.index == undefined) continue;
|
||||||
|
// position in input string is known
|
||||||
|
const index = emote.index;
|
||||||
|
|
||||||
text =
|
text =
|
||||||
text.substring(0, index) +
|
text.substring(0, index) +
|
||||||
`[not]${url}[/not]` +
|
`[not]${url}[/not]` +
|
||||||
text.substring(index + match.length);
|
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]");
|
return text.replace(/\[\/not\]\s+\[not\]/g, "[/not][not]");
|
||||||
}
|
}
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
// find chatbox elements
|
||||||
const cb_form = document.querySelector("#mgc_cb_evo_form");
|
const cb_form = document.querySelector("#mgc_cb_evo_form");
|
||||||
if (!(cb_form instanceof HTMLFormElement)) return;
|
|
||||||
|
|
||||||
const cb_send = document.querySelector(
|
const cb_send = document.querySelector(
|
||||||
"#mgc_cb_evo_form > input[type=image]:nth-child(2)",
|
"#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");
|
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;
|
if (!(cb_input instanceof HTMLInputElement)) return;
|
||||||
|
|
||||||
|
// create "bttv" button
|
||||||
const bttv_btn = (() => {
|
const bttv_btn = (() => {
|
||||||
const btn = document.createElement("a");
|
const btn = document.createElement("a");
|
||||||
btn.style.setProperty("cursor", "pointer");
|
btn.style.setProperty("cursor", "pointer");
|
||||||
|
@ -50,6 +68,9 @@ async function process_text(text: string): Promise<string> {
|
||||||
img.setAttribute("title", "bttv:* emotes ersetzen und absenden");
|
img.setAttribute("title", "bttv:* emotes ersetzen und absenden");
|
||||||
img.style.setProperty("vertical-align", "middle");
|
img.style.setProperty("vertical-align", "middle");
|
||||||
|
|
||||||
|
// ******************
|
||||||
|
// * business logic *
|
||||||
|
// ******************
|
||||||
btn.addEventListener("click", async () => {
|
btn.addEventListener("click", async () => {
|
||||||
cb_input.value = await process_text(cb_input.value);
|
cb_input.value = await process_text(cb_input.value);
|
||||||
cb_input.focus();
|
cb_input.focus();
|
||||||
|
|
Loading…
Reference in a new issue