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