mirror of
https://github.com/qwerinope/qweribot.git
synced 2025-12-19 08:41:39 +01:00
refactor chatwidget code, add prefetching emotes
This commit is contained in:
@@ -6,7 +6,7 @@ CLIENT_SECRET= # Client_secret gotten from the twitch dev console
|
|||||||
# REDIRECT_URL= # Redirect URL that has been set in the twitch dev console. Defaults to: http://localhost:3456
|
# REDIRECT_URL= # Redirect URL that has been set in the twitch dev console. Defaults to: http://localhost:3456
|
||||||
# REDIRECT_PORT= # Redirect port if the REDIRECT_URL has not been set. Defaults to 3456. This is also the port the bot will listen on to authenticate
|
# REDIRECT_PORT= # Redirect port if the REDIRECT_URL has not been set. Defaults to 3456. This is also the port the bot will listen on to authenticate
|
||||||
# COMMAND_PREFIX= # The prefix which will be used to activate commands. Defaults to '!'. When requiring a space between prefix and command, escape the space with a backslash
|
# COMMAND_PREFIX= # The prefix which will be used to activate commands. Defaults to '!'. When requiring a space between prefix and command, escape the space with a backslash
|
||||||
CHATWIDGET_PORT= # The port that the chat widget will be served on
|
WEB_PORT= # The port that the chat widget and sound alerts will be served on
|
||||||
|
|
||||||
# The Twitch IDs required below can be gotten from this website: https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/
|
# The Twitch IDs required below can be gotten from this website: https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/
|
||||||
|
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
import { chatterApi, streamerId } from "..";
|
|
||||||
import logger from "../lib/logger";
|
|
||||||
import type { twitchEventData } from "./websockettypes";
|
|
||||||
import chatWidget from "./www/index.html";
|
|
||||||
|
|
||||||
type badgeObject = {
|
|
||||||
[key: string]: {
|
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type emoteObject = {
|
|
||||||
[key: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = Number(process.env.CHATWIDGET_PORT);
|
|
||||||
if (isNaN(port)) { logger.enverr("CHATWIDGET_PORT"); process.exit(1); };
|
|
||||||
|
|
||||||
const server = Bun.serve({
|
|
||||||
port,
|
|
||||||
fetch(request, server) {
|
|
||||||
if (server.upgrade(request)) return;
|
|
||||||
return new Response('oops', { status: 500 });
|
|
||||||
},
|
|
||||||
routes: {
|
|
||||||
"/": chatWidget,
|
|
||||||
"/getBadges": async () => {
|
|
||||||
const globalBadges = chatterApi.chat.getGlobalBadges();
|
|
||||||
const channelBadges = chatterApi.chat.getChannelBadges(streamerId);
|
|
||||||
const rawBadges = await Promise.all([globalBadges, channelBadges]);
|
|
||||||
|
|
||||||
const newObj: badgeObject = {};
|
|
||||||
parseRawBadges(newObj, rawBadges[0]);
|
|
||||||
parseRawBadges(newObj, rawBadges[1]);
|
|
||||||
|
|
||||||
return Response.json(newObj);
|
|
||||||
},
|
|
||||||
"/getEmotes": async () => {
|
|
||||||
const [bttvglobal, bttvuser, ffzglobal, ffzuser, seventvglobal, seventvuser] = await Promise.all([
|
|
||||||
fetch("https://api.betterttv.net/3/cached/emotes/global").then(a => a.json() as any),
|
|
||||||
fetch("https://api.betterttv.net/3/cached/users/twitch/" + streamerId).then(a => a.json() as any),
|
|
||||||
fetch("https://api.frankerfacez.com/v1/set/global").then(a => a.json() as any),
|
|
||||||
fetch("https://api.frankerfacez.com/v1/room/id/" + streamerId).then(a => a.json() as any),
|
|
||||||
fetch("https://7tv.io/v3/emote-sets/global").then(a => a.json() as any),
|
|
||||||
fetch("https://7tv.io/v3/users/twitch/" + streamerId).then(a => a.json() as any)
|
|
||||||
]);
|
|
||||||
const emotes: emoteObject = {};
|
|
||||||
for (const a of bttvglobal) {
|
|
||||||
emotes[a.code] = `https://cdn.betterttv.net/emote/${a.id}/3x.${a.imageType}`;
|
|
||||||
};
|
|
||||||
for (const a of bttvuser.sharedEmotes) {
|
|
||||||
emotes[a.code] = `https://cdn.betterttv.net/emote/${a.id}/3x.${a.imageType}`;
|
|
||||||
};
|
|
||||||
for (const a of ffzglobal.default_sets) {
|
|
||||||
for (const b of ffzglobal.sets[a].emoticons) {
|
|
||||||
emotes[b.name] = `https://cdn.frankerfacez.com/emote/${b.id}/4`;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
for (const a of ffzuser.sets[ffzuser.room.set].emoticons) {
|
|
||||||
emotes[a.name] = `https://cdn.frankerfacez.com/emote/${a.id}/4`;
|
|
||||||
}
|
|
||||||
for (const a of seventvglobal.emotes) {
|
|
||||||
emotes[a.name] = `https://cdn.7tv.app/emote/${a.id}/4x.avif`;
|
|
||||||
};
|
|
||||||
for (const a of seventvuser.emote_set.emotes) {
|
|
||||||
emotes[a.name] = `https://cdn.7tv.app/emote/${a.id}/4x.avif`;
|
|
||||||
};
|
|
||||||
return Response.json(emotes);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
websocket: {
|
|
||||||
open(_ws) {
|
|
||||||
sendTwitchEvent({
|
|
||||||
function: 'serverNotification',
|
|
||||||
message: 'Sucessfully opened websocket connection'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
message(ws, omessage) {
|
|
||||||
const message = JSON.parse(omessage.toString());
|
|
||||||
if (!message.type) return;
|
|
||||||
switch (message.type) {
|
|
||||||
case 'subscribe':
|
|
||||||
if (!message.target) return;
|
|
||||||
ws.subscribe(message.target);
|
|
||||||
sendTwitchEvent({
|
|
||||||
function: 'serverNotification',
|
|
||||||
message: `Successfully subscribed to all ${message.target} events`
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
close(ws) {
|
|
||||||
ws.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
development: true,
|
|
||||||
error(error) {
|
|
||||||
logger.err(`Error at chatwidget server: ${error}`);
|
|
||||||
return new Response("Internal Server Error", { status: 500 })
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function sendTwitchEvent(event: twitchEventData) {
|
|
||||||
server.publish('twitch', JSON.stringify(event));
|
|
||||||
};
|
|
||||||
|
|
||||||
import { HelixChatBadgeSet } from "@twurple/api";
|
|
||||||
|
|
||||||
function parseRawBadges(returnobj: badgeObject, data: HelixChatBadgeSet[]) {
|
|
||||||
for (const badge of data) {
|
|
||||||
if (!returnobj[badge.id]) returnobj[badge.id] = {};
|
|
||||||
for (const version of badge.versions) {
|
|
||||||
returnobj[badge.id]![version.id] = version.getImageUrl(4);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { eventSub, streamerId } from "..";
|
import { eventSub, streamerId } from "..";
|
||||||
import { deleteBannedUserMessagesFromChatWidget } from "../chatwidget/message";
|
import { deleteBannedUserMessagesFromChatWidget } from "../web/chatWidget/message";
|
||||||
|
|
||||||
eventSub.onChannelBan(streamerId, async msg => {
|
eventSub.onChannelBan(streamerId, async msg => {
|
||||||
deleteBannedUserMessagesFromChatWidget(msg);
|
deleteBannedUserMessagesFromChatWidget(msg);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { eventSub, streamerId } from "..";
|
import { eventSub, streamerId } from "..";
|
||||||
import { deleteMessageFromChatWidget } from "../chatwidget/message";
|
import { deleteMessageFromChatWidget } from "../web/chatWidget/message";
|
||||||
|
|
||||||
eventSub.onChannelChatMessageDelete(streamerId, streamerId, async msg => {
|
eventSub.onChannelChatMessageDelete(streamerId, streamerId, async msg => {
|
||||||
deleteMessageFromChatWidget(msg);
|
deleteMessageFromChatWidget(msg);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base"
|
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base"
|
||||||
import { chatterId, streamerId, eventSub, commandPrefix, singleUserMode, streamerUsers } from "..";
|
import { streamerId, eventSub, commandPrefix, streamerUsers } from "..";
|
||||||
import { User } from "../user";
|
import { User } from "../user";
|
||||||
import commands, { sendMessage } from "../commands";
|
import commands, { sendMessage } from "../commands";
|
||||||
import { redis } from "bun";
|
import { redis } from "bun";
|
||||||
import { isAdmin } from "../lib/admins";
|
import { isAdmin } from "../lib/admins";
|
||||||
import cheers from "../cheers";
|
import cheers from "../cheers";
|
||||||
import logger from "../lib/logger";
|
import logger from "../lib/logger";
|
||||||
import { addMessageToChatWidget } from "../chatwidget/message";
|
import { addMessageToChatWidget } from "../web/chatWidget/message";
|
||||||
import { isInvuln } from "../lib/invuln";
|
import { isInvuln } from "../lib/invuln";
|
||||||
|
|
||||||
logger.info(`Loaded the following commands: ${commands.keys().toArray().join(', ')}`);
|
logger.info(`Loaded the following commands: ${commands.keys().toArray().join(', ')}`);
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ streamerUsers.forEach(async id => await Promise.all([addAdmin(id), addInvuln(id)
|
|||||||
|
|
||||||
await import("./events");
|
await import("./events");
|
||||||
|
|
||||||
await import("./chatwidget");
|
await import("./web");
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { EventSubChannelChatMessageEvent, EventSubChannelChatMessageDeleteEvent, EventSubChannelBanEvent } from "@twurple/eventsub-base";
|
import { EventSubChannelChatMessageEvent, EventSubChannelChatMessageDeleteEvent, EventSubChannelBanEvent } from "@twurple/eventsub-base";
|
||||||
import { sendTwitchEvent } from ".";
|
import { sendTwitchChatEvent } from "./widgetServerFunctions";
|
||||||
|
|
||||||
export async function addMessageToChatWidget(msg: EventSubChannelChatMessageEvent) {
|
export async function addMessageToChatWidget(msg: EventSubChannelChatMessageEvent) {
|
||||||
await sendTwitchEvent({
|
await sendTwitchChatEvent({
|
||||||
function: 'createMessage',
|
function: 'createMessage',
|
||||||
messageParts: msg.messageParts,
|
messageParts: msg.messageParts,
|
||||||
displayName: msg.chatterDisplayName,
|
displayName: msg.chatterDisplayName,
|
||||||
@@ -14,14 +14,14 @@ export async function addMessageToChatWidget(msg: EventSubChannelChatMessageEven
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function deleteMessageFromChatWidget(msg: EventSubChannelChatMessageDeleteEvent) {
|
export async function deleteMessageFromChatWidget(msg: EventSubChannelChatMessageDeleteEvent) {
|
||||||
await sendTwitchEvent({
|
await sendTwitchChatEvent({
|
||||||
function: 'deleteMessage',
|
function: 'deleteMessage',
|
||||||
messageId: msg.messageId
|
messageId: msg.messageId
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function deleteBannedUserMessagesFromChatWidget(msg: EventSubChannelBanEvent) {
|
export async function deleteBannedUserMessagesFromChatWidget(msg: EventSubChannelBanEvent) {
|
||||||
sendTwitchEvent({
|
sendTwitchChatEvent({
|
||||||
function: 'userBan',
|
function: 'userBan',
|
||||||
chatterId: msg.userId
|
chatterId: msg.userId
|
||||||
});
|
});
|
||||||
74
src/web/chatWidget/widgetServerFunctions.ts
Normal file
74
src/web/chatWidget/widgetServerFunctions.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { streamerId, chatterApi } from "../..";
|
||||||
|
|
||||||
|
type badgeObject = {
|
||||||
|
[key: string]: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type emoteObject = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getBadges() {
|
||||||
|
const globalBadges = chatterApi.chat.getGlobalBadges();
|
||||||
|
const channelBadges = chatterApi.chat.getChannelBadges(streamerId);
|
||||||
|
const rawBadges = await Promise.all([globalBadges, channelBadges]);
|
||||||
|
|
||||||
|
const newObj: badgeObject = {};
|
||||||
|
parseRawBadges(newObj, rawBadges[0]);
|
||||||
|
parseRawBadges(newObj, rawBadges[1]);
|
||||||
|
|
||||||
|
return Response.json(newObj);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getExternalEmotes() {
|
||||||
|
const [bttvglobal, bttvuser, ffzglobal, ffzuser, seventvglobal, seventvuser] = await Promise.all([
|
||||||
|
fetch("https://api.betterttv.net/3/cached/emotes/global").then(a => a.json() as any),
|
||||||
|
fetch("https://api.betterttv.net/3/cached/users/twitch/" + streamerId).then(a => a.json() as any),
|
||||||
|
fetch("https://api.frankerfacez.com/v1/set/global").then(a => a.json() as any),
|
||||||
|
fetch("https://api.frankerfacez.com/v1/room/id/" + streamerId).then(a => a.json() as any),
|
||||||
|
fetch("https://7tv.io/v3/emote-sets/global").then(a => a.json() as any),
|
||||||
|
fetch("https://7tv.io/v3/users/twitch/" + streamerId).then(a => a.json() as any)
|
||||||
|
]);
|
||||||
|
const emotes: emoteObject = {};
|
||||||
|
for (const a of bttvglobal) {
|
||||||
|
emotes[a.code] = `https://cdn.betterttv.net/emote/${a.id}/3x.${a.imageType}`;
|
||||||
|
};
|
||||||
|
for (const a of bttvuser.sharedEmotes) {
|
||||||
|
emotes[a.code] = `https://cdn.betterttv.net/emote/${a.id}/3x.${a.imageType}`;
|
||||||
|
};
|
||||||
|
for (const a of ffzglobal.default_sets) {
|
||||||
|
for (const b of ffzglobal.sets[a].emoticons) {
|
||||||
|
emotes[b.name] = `https://cdn.frankerfacez.com/emote/${b.id}/4`;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
for (const a of ffzuser.sets[ffzuser.room.set].emoticons) {
|
||||||
|
emotes[a.name] = `https://cdn.frankerfacez.com/emote/${a.id}/4`;
|
||||||
|
}
|
||||||
|
for (const a of seventvglobal.emotes) {
|
||||||
|
emotes[a.name] = `https://cdn.7tv.app/emote/${a.id}/4x.avif`;
|
||||||
|
};
|
||||||
|
for (const a of seventvuser.emote_set.emotes) {
|
||||||
|
emotes[a.name] = `https://cdn.7tv.app/emote/${a.id}/4x.avif`;
|
||||||
|
};
|
||||||
|
return Response.json(emotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
import { HelixChatBadgeSet } from "@twurple/api";
|
||||||
|
|
||||||
|
function parseRawBadges(returnobj: badgeObject, data: HelixChatBadgeSet[]) {
|
||||||
|
for (const badge of data) {
|
||||||
|
if (!returnobj[badge.id]) returnobj[badge.id] = {};
|
||||||
|
for (const version of badge.versions) {
|
||||||
|
returnobj[badge.id]![version.id] = version.getImageUrl(4);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
import server from "..";
|
||||||
|
import type { twitchEventData } from "./websockettypes";
|
||||||
|
|
||||||
|
export async function sendTwitchChatEvent(event: twitchEventData) {
|
||||||
|
server.publish('twitchchat', JSON.stringify(event));
|
||||||
|
};
|
||||||
@@ -1,5 +1,47 @@
|
|||||||
const badges = await fetch(`http://${location.host}/getBadges`).then(data => data.json());
|
const popover = document.createElement('div')
|
||||||
const emotes = await fetch(`http://${location.host}/getEmotes`).then(data => data.json());
|
Object.assign(popover.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
top: '20px',
|
||||||
|
right: '20px',
|
||||||
|
background: 'rgba(0, 0, 0, 0.85)',
|
||||||
|
color: 'white',
|
||||||
|
padding: '10px 20px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
fontSize: '9vmin',
|
||||||
|
zIndex: 9999
|
||||||
|
});
|
||||||
|
popover.textContent = 'Loading...'
|
||||||
|
document.body.appendChild(popover);
|
||||||
|
|
||||||
|
const [badges, emotes] = await Promise.all([
|
||||||
|
fetch(`http://${location.host}/chat/getBadges`).then(data => data.json()),
|
||||||
|
fetch(`http://${location.host}/chat/getEmotes`).then(data => data.json())
|
||||||
|
]);
|
||||||
|
|
||||||
|
await prefetchImages(Object.values(emotes));
|
||||||
|
|
||||||
|
popover.remove();
|
||||||
|
|
||||||
|
async function prefetchImages(urls: string[], maxRetries = 3, retryDelay = 500) {
|
||||||
|
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
|
||||||
|
|
||||||
|
const loadImage = async (url: string) => {
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve(true);
|
||||||
|
img.onerror = () => reject();
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (attempt < maxRetries) await sleep(retryDelay);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(urls.map(url => loadImage(url)));
|
||||||
|
};
|
||||||
|
|
||||||
import { type createMessageEvent } from '../../websockettypes';
|
import { type createMessageEvent } from '../../websockettypes';
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ const socket = new WebSocket(`ws://${location.host}`);
|
|||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
type: 'subscribe',
|
type: 'subscribe',
|
||||||
target: 'twitch'
|
target: 'twitchchat'
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
51
src/web/index.ts
Normal file
51
src/web/index.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import logger from "../lib/logger";
|
||||||
|
import { getBadges, getExternalEmotes } from "./chatWidget/widgetServerFunctions";
|
||||||
|
import chatWidget from "./chatWidget/www/index.html";
|
||||||
|
import { sendTwitchChatEvent } from "./chatWidget/widgetServerFunctions";
|
||||||
|
|
||||||
|
const port = Number(process.env.WEB_PORT);
|
||||||
|
if (isNaN(port)) { logger.enverr("WEB_PORT"); process.exit(1); };
|
||||||
|
|
||||||
|
export default Bun.serve({
|
||||||
|
port,
|
||||||
|
fetch(request, server) {
|
||||||
|
if (server.upgrade(request)) return;
|
||||||
|
return new Response('oops', { status: 500 });
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
"/chat": chatWidget,
|
||||||
|
"/chat/getBadges": getBadges,
|
||||||
|
"/chat/getEmotes": getExternalEmotes
|
||||||
|
},
|
||||||
|
websocket: {
|
||||||
|
open(_ws) {
|
||||||
|
sendTwitchChatEvent({
|
||||||
|
function: 'serverNotification',
|
||||||
|
message: 'Sucessfully opened websocket connection'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
message(ws, omessage) {
|
||||||
|
const message = JSON.parse(omessage.toString());
|
||||||
|
if (!message.type) return;
|
||||||
|
switch (message.type) {
|
||||||
|
case 'subscribe':
|
||||||
|
if (!message.target) return;
|
||||||
|
ws.subscribe(message.target);
|
||||||
|
sendTwitchChatEvent({
|
||||||
|
function: 'serverNotification',
|
||||||
|
message: `Successfully subscribed to all ${message.target} events`
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
close(ws) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
development: true,
|
||||||
|
error(error) {
|
||||||
|
logger.err(`Error at chatwidget server: ${error}`);
|
||||||
|
return new Response("Internal Server Error", { status: 500 })
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user