implement a basic alerts template

This commit is contained in:
2025-07-25 23:54:02 +01:00
parent a9c89cd616
commit 15c8abc2c3
15 changed files with 194 additions and 34 deletions

View File

@@ -0,0 +1,13 @@
import server from "web";
import type { alert, alertEventData } from "./types";
export async function sendAlertEvent(event: alertEventData) {
server.publish('alerts', JSON.stringify(event));
};
export async function playAlert(alert: alert) {
await sendAlertEvent({
function: 'playAlert',
alert
});
};

37
src/web/alerts/types.ts Normal file
View File

@@ -0,0 +1,37 @@
import type { serverNotificationEvent } from "web/serverTypes";
type alertBase<name extends string> = {
name: name;
user: string;
};
export type userBlastAlert = alertBase<'userBlast'> & {
target: string;
};
export type userExecutionAlert = alertBase<'userExecution'> & {
target: string;
};
export type grenadeExplosionAlert = alertBase<'grenadeExplosion'> & {
target: string;
};
export type tntExplosionAlert = alertBase<'tntExplosion'> & {
targets: string[];
};
export type alert =
| userBlastAlert
| userExecutionAlert
| grenadeExplosionAlert
| tntExplosionAlert;
type playAlertEvent = {
function: 'playAlert';
alert: alert;
};
export type alertEventData =
| playAlertEvent
| serverNotificationEvent;

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>qwerinope's alert server</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,30 @@
import { alert } from "web/alerts/types";
import alerts from "./index";
class AlertManager {
#busy: boolean;
#alertQueue: alert[];
constructor() {
this.#busy = false;
this.#alertQueue = [];
};
private async playAlert(alert: alert) {
this.#busy = true;
await alerts[alert.name](alert);
const nextAlert = this.#alertQueue.shift();
if (!nextAlert) { this.#busy = false; return; }; // if .shift has no results, we done and return
this.playAlert(nextAlert);
};
async queueAlert(alert: alert) {
if (this.#busy) { this.#alertQueue.push(alert); return; };
await this.playAlert(alert);
};
};
export default new AlertManager();

View File

@@ -0,0 +1,14 @@
export function delay(time: number) {
return new Promise(function(resolve) {
setTimeout(resolve, time)
});
};
import userBlast from "./userBlast";
export default {
'userBlast': userBlast,
'userExecute': userBlast,
'grenadeExplosion': userBlast,
'tntExplosion': userBlast
}

View File

@@ -0,0 +1,18 @@
import { userBlastAlert } from "web/alerts/types";
import { delay } from "./index";
export default async function execute(alert: userBlastAlert) {
const parentDiv = document.createElement('div');
const textElement = document.createElement('span');
textElement.textContent = `${alert.user} just blasted ${alert.target} for 60 seconds! Rip bozo!`;
parentDiv.appendChild(textElement);
Object.assign(textElement.style, {
position: 'fixed',
top: '20px',
left: '20px',
zIndex: 1000
});
document.querySelector("#app").appendChild(parentDiv);
await delay(10000);
parentDiv.remove();
};

View File

@@ -0,0 +1,25 @@
import { serverInstruction } from "web/serverTypes";
import { alertEventData } from "web/alerts/types";
import alertManager from "./alerts/alertManager";
const socket = new WebSocket(`ws://${location.host}`);
socket.onopen = () => {
const instruction: serverInstruction = {
type: 'subscribe',
target: 'alerts'
};
socket.send(JSON.stringify(instruction));
};
socket.onmessage = event => {
const data: alertEventData = JSON.parse(event.data);
switch (data.function) {
case "playAlert":
alertManager.queueAlert(data.alert);
break;
case 'serverNotification':
console.log(data.message);
break;
};
};

View File

@@ -1,3 +1,5 @@
import type { serverNotificationEvent } from "web/serverTypes";
export type createMessageEvent = { export type createMessageEvent = {
function: 'createMessage'; function: 'createMessage';
messageParts: EventSubChatMessagePart[]; messageParts: EventSubChatMessagePart[];
@@ -12,17 +14,14 @@ export type deleteMessageEvent = {
function: 'deleteMessage'; function: 'deleteMessage';
messageId: string; messageId: string;
}; };
export type userBanEvent = { export type userBanEvent = {
function: 'userBan'; function: 'userBan';
chatterId: string; chatterId: string;
}; };
export type serverNotificationEvent = {
function: 'serverNotification';
message: string;
};
export type twitchEventData = export type twitchEventData =
createMessageEvent | createMessageEvent
| deleteMessageEvent | deleteMessageEvent
| userBanEvent | userBanEvent
| serverNotificationEvent; | serverNotificationEvent;
@@ -32,44 +31,44 @@ export type twitchEventData =
export interface EventSubChatMessageTextPart { export interface EventSubChatMessageTextPart {
type: 'text'; type: 'text';
text: string; text: string;
} };
export interface EventSubChatMessageCheermote { export interface EventSubChatMessageCheermote {
prefix: string; prefix: string;
bits: number; bits: number;
tier: number; tier: number;
} };
export interface EventSubChatMessageCheermotePart { export interface EventSubChatMessageCheermotePart {
type: 'cheermote'; type: 'cheermote';
text: string; text: string;
cheermote: EventSubChatMessageCheermote; cheermote: EventSubChatMessageCheermote;
} };
export interface EventSubChatMessageEmote { export interface EventSubChatMessageEmote {
id: string; id: string;
emote_set_id: string; emote_set_id: string;
owner_id: string; owner_id: string;
format: string[]; format: string[];
} };
export interface EventSubChatMessageEmotePart { export interface EventSubChatMessageEmotePart {
type: 'emote'; type: 'emote';
text: string; text: string;
emote: EventSubChatMessageEmote; emote: EventSubChatMessageEmote;
} };
export interface EventSubChatMessageMention { export interface EventSubChatMessageMention {
user_id: string; user_id: string;
user_name: string; user_name: string;
user_login: string; user_login: string;
} };
export interface EventSubChatMessageMentionPart { export interface EventSubChatMessageMentionPart {
type: 'mention'; type: 'mention';
text: string; text: string;
mention: EventSubChatMessageMention; mention: EventSubChatMessageMention;
} };
export type EventSubChatMessagePart = export type EventSubChatMessagePart =
| EventSubChatMessageTextPart | EventSubChatMessageTextPart

View File

@@ -84,5 +84,5 @@ import server from "web";
import type { twitchEventData } from "web/chatWidget/websockettypes"; import type { twitchEventData } from "web/chatWidget/websockettypes";
export async function sendTwitchChatEvent(event: twitchEventData) { export async function sendTwitchChatEvent(event: twitchEventData) {
server.publish('twitchchat', JSON.stringify(event)); server.publish('twitch.chat', JSON.stringify(event));
}; };

View File

@@ -3,14 +3,16 @@ import '@fontsource/jersey-15';
import { type twitchEventData } from "web/chatWidget/websockettypes"; import { type twitchEventData } from "web/chatWidget/websockettypes";
import { parseMessage } from './createMessage'; import { parseMessage } from './createMessage';
import { serverInstruction } from 'web/serverTypes';
const socket = new WebSocket(`ws://${location.host}`); const socket = new WebSocket(`ws://${location.host}`);
socket.onopen = () => { socket.onopen = () => {
socket.send(JSON.stringify({ const instruction: serverInstruction = {
type: 'subscribe', type: 'subscribe',
target: 'twitchchat' target: 'twitch.chat'
})); };
socket.send(JSON.stringify(instruction));
}; };
socket.onmessage = event => { socket.onmessage = event => {

View File

@@ -1,7 +1,8 @@
import logger from "lib/logger"; import logger from "lib/logger";
import { getBadges, getExternalEmotes } from "web/chatWidget/widgetServerFunctions";
import chatWidget from "web/chatWidget/www/index.html"; import chatWidget from "web/chatWidget/www/index.html";
import { sendTwitchChatEvent } from "web/chatWidget/widgetServerFunctions"; import { getBadges, getExternalEmotes } from "web/chatWidget/widgetServerFunctions";
import alerts from "web/alerts/www/index.html";
import type { serverInstruction, serverNotificationEvent } from "web/serverTypes";
const port = Number(process.env.WEB_PORT); const port = Number(process.env.WEB_PORT);
if (isNaN(port)) { logger.enverr("WEB_PORT"); process.exit(1); }; if (isNaN(port)) { logger.enverr("WEB_PORT"); process.exit(1); };
@@ -15,26 +16,23 @@ export default Bun.serve({
routes: { routes: {
"/chat": chatWidget, "/chat": chatWidget,
"/chat/getBadges": getBadges, "/chat/getBadges": getBadges,
"/chat/getEmotes": getExternalEmotes "/chat/getEmotes": getExternalEmotes,
"/alerts": alerts
}, },
websocket: { websocket: {
open(_ws) {
sendTwitchChatEvent({
function: 'serverNotification',
message: 'Sucessfully opened websocket connection'
});
},
message(ws, omessage) { message(ws, omessage) {
const message = JSON.parse(omessage.toString()); const message = JSON.parse(omessage.toString()) as serverInstruction;
if (!message.type) return; if (!message.type) return;
switch (message.type) { switch (message.type) {
case 'subscribe': case 'subscribe':
if (!message.target) return; if (!message.target) return;
const target = message.target.toLowerCase();
ws.subscribe(message.target); ws.subscribe(message.target);
sendTwitchChatEvent({ ws.send(JSON.stringify({
function: 'serverNotification', function: 'serverNotification',
message: `Successfully subscribed to all ${message.target} events` message: `Successfully subscribed to ${target} events`
}); } as serverNotificationEvent)); // Both alerts and chatwidget eventsub subscriptions have the notification field
break; break;
}; };
}, },

13
src/web/serverTypes.ts Normal file
View File

@@ -0,0 +1,13 @@
type subscribe = {
type: 'subscribe',
target: string;
};
export type serverInstruction =
| subscribe;
// This event is found on all listeners, so it should be placed here
export type serverNotificationEvent = {
function: 'serverNotification';
message: string;
};

View File

@@ -19,5 +19,5 @@
"noPropertyAccessFromIndexSignature": false "noPropertyAccessFromIndexSignature": false
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["src/web/chatWidget/www/**/*"] "exclude": ["src/web/chatWidget/www/**/*", "src/web/alerts/www/**/*"]
} }

View File

@@ -15,6 +15,6 @@
}, },
"references": [ "references": [
{ "path": "./tsconfig.bot.json" }, { "path": "./tsconfig.bot.json" },
{ "path": "./tsconfig.chatwidget.json" } { "path": "./tsconfig.web.json" }
] ]
} }

View File

@@ -3,8 +3,7 @@
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"lib": ["DOM", "ES2020"], "lib": ["DOM", "ES2020"]
"types": []
}, },
"include": ["src/web/chatWidget/www/**/*"] "include": ["src/web/chatWidget/www/**/*", "src/web/alerts/www/**/*"]
} }