From f23e8cb81b7d28a78470f93cbc2943bc91324001 Mon Sep 17 00:00:00 2001 From: alexbcberio Date: Fri, 18 Jun 2021 17:09:10 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extracted=20chatClient=20a?= =?UTF-8?q?nd=20scheduledActions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.ts | 153 +----------------------- src/backend/chatClient/index.ts | 145 ++++++++++++++++++++++ src/backend/helpers/scheduledActions.ts | 74 ++++++++++++ src/backend/webServer/index.ts | 9 +- 4 files changed, 228 insertions(+), 153 deletions(-) create mode 100644 src/backend/chatClient/index.ts create mode 100644 src/backend/helpers/scheduledActions.ts diff --git a/index.ts b/index.ts index ea6f2ce..03a7c17 100644 --- a/index.ts +++ b/index.ts @@ -1,28 +1,14 @@ import { PubSubClient, PubSubRedemptionMessage } from "twitch-pubsub-client"; +import { broadcast, chatClient, connect, say, } from "./src/backend/chatClient"; import { getApiClient, getAuthProvider } from "./src/backend/helpers/twitch"; -import { listen, sockets } from "./src/backend/webServer"; +import { saveScheduledActions, scheduledActions } from "./src/backend/helpers/scheduledActions"; import { ApiClient } from "twitch"; -import { ChatClient } from "twitch-chat-client"; -import { promises as fs } from "fs"; - -const SCHEDULED_FILE = "./scheduled.json"; - -const scheduledActions: Array = []; -let saInterval: NodeJS.Timeout; +import { listen, } from "./src/backend/webServer"; const channel = "alexbcberio"; let apiClient: ApiClient; -let chatClient: ChatClient; - -export { - handleClientAction, - scheduledActions, - saveScheduledActions -} - -//! Important: store users & channels by id, not by username async function init() { const authProvider = await getAuthProvider(); @@ -35,46 +21,13 @@ async function init() { console.log("[Twitch PubSub] Connected & registered"); - chatClient = new ChatClient(authProvider, { channels: [channel] }); - - chatClient.onConnect(onConnect); - - chatClient.onDisconnect((e: any) => { - console.log(`[ChatClient] Disconnected ${e.message}`); - }); - - chatClient.onNoPermission((channel, message) => { - console.log(`[ChatClient] No permission on ${channel}: ${message}`); - }); - - chatClient.connect(); + await connect(authProvider, [channel]); listen(); } init(); -async function onConnect() { - console.log("[ChatClient] Connected"); - - // *Check this, not working - if (!saInterval) { - let savedActions = []; - try { - savedActions = JSON.parse( - (await fs.readFile(SCHEDULED_FILE)).toString() - ); - } catch (e) { - // probably file does not exist - } - scheduledActions.push.apply(scheduledActions, savedActions); - scheduledActions.sort((a, b) => a.scheduledAt - b.scheduledAt); - - setTimeout(checkScheduledActions, 1000 * 5); - saInterval = setInterval(checkScheduledActions, 1000 * 60); - } -} - async function onRedemption(message: PubSubRedemptionMessage) { console.log( `Reward: "${message.rewardName}" (${message.rewardId}) redeemed by ${message.userDisplayName}` @@ -110,104 +63,6 @@ async function onRedemption(message: PubSubRedemptionMessage) { } } -async function handleClientAction(action: any) { - if (action.channel && !isNaN(action.channel)) { - action.channel = await getUsernameFromId(parseInt(action.channel)); - } - if (action.username && !isNaN(action.username)) { - action.username = await getUsernameFromId(parseInt(action.username)); - } - - switch (action.action) { - case "say": - say(channel, action.message); - break; - case "timeout": - await timeout(channel, action.username, action.time, action.reason); - break; - case "broadcast": - broadcast(action.message); - break; - case "addVip": - await addVip(action.channel, action.username); - break; - case "removeVip": - await removeVip(action.channel, action.username); - break; - default: - console.log(`Couldn't handle action:`, action); - } -} - -let ssaTimeout: NodeJS.Timeout | null; -function saveScheduledActions() { - if (ssaTimeout) { - clearTimeout(ssaTimeout); - ssaTimeout = null; - console.log("[Scheduled] Removed save timeout."); - } - - ssaTimeout = setTimeout(async () => { - await fs.writeFile(SCHEDULED_FILE, JSON.stringify(scheduledActions)); - console.log("[Scheduled] Saved actions."); - ssaTimeout = null; - }, 1000 * 30); -} - -let checkingScheduled = false; -async function checkScheduledActions() { - if (checkingScheduled) return; - checkingScheduled = true; - - let hasToSave = false; - - for (let i = 0; i < scheduledActions.length && scheduledActions[i].scheduledAt <= Date.now(); i++) { - hasToSave = true; - - const action = scheduledActions.splice(i, 1)[0]; - await handleClientAction(action); - console.log(`[Scheduled] Executed: ${JSON.stringify(action)}`); - } - - if (hasToSave) { - saveScheduledActions(); - } - - checkingScheduled = false; -} - -// send a chat message -function say(channel: string, message: string) { - chatClient.say(channel, message); -} - -// timeouts a user in a channel -async function timeout( - channel: string, - username: string, - time?: number, - reason?: string -) { - if (!time) { - time = 60; - } - - if (!reason) { - reason = ""; - } - - try { - await chatClient.timeout(channel, username, time, reason); - } catch (e) { - // user cannot be timed out - } -} - -// broadcast a message to all clients -function broadcast(msg: object) { - sockets.forEach(s => s.send(JSON.stringify(msg))); -} - // adds a user to vips async function addVip(channel: string, username: string, message?: string) { if (!message) { diff --git a/src/backend/chatClient/index.ts b/src/backend/chatClient/index.ts new file mode 100644 index 0000000..314788a --- /dev/null +++ b/src/backend/chatClient/index.ts @@ -0,0 +1,145 @@ +import { AuthProvider } from "twitch-auth"; +import { ChatClient } from "twitch-chat-client"; +import { getApiClient } from "../helpers/twitch"; +import { sockets } from "../webServer"; +import { start } from "../helpers/scheduledActions"; + +let chatClient: ChatClient; + +export { + chatClient, + connect, + handleClientAction, + broadcast, + say +}; + +async function connect(authProvider: AuthProvider, channels: Array) { + if ( + !chatClient || + chatClient.isConnecting || + chatClient.isConnected + ) { + return; + } + + chatClient = new ChatClient(authProvider, { channels: channels }); + + chatClient.onConnect(onConnect); + + chatClient.onDisconnect((e: any) => { + console.log(`[ChatClient] Disconnected ${e.message}`); + }); + + chatClient.onNoPermission((channel, message) => { + console.log(`[ChatClient] No permission on ${channel}: ${message}`); + }); + + await chatClient.connect(); +} + +async function onConnect() { + console.log("[ChatClient] Connected"); + + start(); +} + +async function handleClientAction(action: any) { + if (action.channel && !isNaN(action.channel)) { + action.channel = await getUsernameFromId(parseInt(action.channel)); + } + if (action.username && !isNaN(action.username)) { + action.username = await getUsernameFromId(parseInt(action.username)); + } + + switch (action.action) { + case "say": + // TODO: check if it works + // say(channel, action.message); + say(action.channel, action.message); + break; + case "timeout": + // TODO: check if it works + // await timeout(channel, action.username, action.time, action.reason); + await timeout(action.channel, action.username, action.time, action.reason); + break; + case "broadcast": + broadcast(action.message); + break; + case "addVip": + await addVip(action.channel, action.username); + break; + case "removeVip": + await removeVip(action.channel, action.username); + break; + default: + console.log(`Couldn't handle action:`, action); + } +} + +// send a chat message +function say(channel: string, message: string) { + chatClient.say(channel, message); +} + +// timeouts a user in a channel +async function timeout( + channel: string, + username: string, + time?: number, + reason?: string +) { + if (!time) { + time = 60; + } + + if (!reason) { + reason = ""; + } + + try { + await chatClient.timeout(channel, username, time, reason); + } catch (e) { + // user cannot be timed out + } +} + +// broadcast a message to all clients +function broadcast(msg: string, socket?: any) { + const filteredSockets = socket + ? sockets.filter(s => s !== socket) + : sockets; + + filteredSockets.forEach(s => s.send(msg)); +} + +// adds a user to vips +async function addVip(channel: string, username: string, message?: string) { + if (!message) { + message = `Otorgado VIP a @${username}.`; + } + + await chatClient.addVip(channel, username); + say(channel, message); +} + +// removes a user from vips +async function removeVip(channel: string, username: string, message?: string) { + if (!message) { + message = `Se ha acabado el chollo, VIP de @${username} eliminado.`; + } + + await chatClient.removeVip(channel, username); + say(channel, message); +} + +async function getUsernameFromId(userId: number) { + const apiClient = await getApiClient(); + const user = await apiClient.helix.users.getUserById(userId); + + if (!user) { + return null; + } + + return user.displayName; +} \ No newline at end of file diff --git a/src/backend/helpers/scheduledActions.ts b/src/backend/helpers/scheduledActions.ts new file mode 100644 index 0000000..5011ef4 --- /dev/null +++ b/src/backend/helpers/scheduledActions.ts @@ -0,0 +1,74 @@ +import { promises as fs } from "fs"; +import { handleClientAction } from "../chatClient"; +import { resolve } from "path"; + +export { + start, + scheduledActions, + checkScheduledActions, + saveScheduledActions +}; + +const SCHEDULED_FILE = resolve(process.cwd(), "scheduled.json"); +const scheduledActions: Array = []; + +let checkingScheduled = false; +let scheduledActionsInterval: NodeJS.Timeout; +let saveScheduledActionsTimeout: NodeJS.Timeout | null; + +async function start() { + // *Check this, not working + if (!scheduledActionsInterval) { + let savedActions = []; + try { + savedActions = JSON.parse((await fs.readFile(SCHEDULED_FILE)).toString()); + } catch (e) { + // probably file does not exist + } + scheduledActions.push.apply(scheduledActions, savedActions); + scheduledActions.sort((a, b) => a.scheduledAt - b.scheduledAt); + + setTimeout(checkScheduledActions, 1000 * 5); + scheduledActionsInterval = setInterval(checkScheduledActions, 1000 * 60); + } +} + +async function checkScheduledActions() { + if (checkingScheduled) return; + checkingScheduled = true; + + let hasToSave = false; + + for ( + let i = 0; + i < scheduledActions.length && + scheduledActions[i].scheduledAt <= Date.now(); + i++ + ) { + hasToSave = true; + + const action = scheduledActions.splice(i, 1)[0]; + await handleClientAction(action); + console.log(`[Scheduled] Executed: ${JSON.stringify(action)}`); + } + + if (hasToSave) { + saveScheduledActions(); + } + + checkingScheduled = false; +} + +function saveScheduledActions() { + if (saveScheduledActionsTimeout) { + clearTimeout(saveScheduledActionsTimeout); + saveScheduledActionsTimeout = null; + console.log("[Scheduled] Removed save timeout."); + } + + saveScheduledActionsTimeout = setTimeout(async () => { + await fs.writeFile(SCHEDULED_FILE, JSON.stringify(scheduledActions)); + console.log("[Scheduled] Saved actions."); + saveScheduledActionsTimeout = null; + }, 1000 * 30); +} diff --git a/src/backend/webServer/index.ts b/src/backend/webServer/index.ts index c70baf9..db449bd 100644 --- a/src/backend/webServer/index.ts +++ b/src/backend/webServer/index.ts @@ -1,5 +1,6 @@ import { IncomingMessage, Server } from "http"; -import { handleClientAction, saveScheduledActions, scheduledActions } from "../../.."; +import { broadcast, handleClientAction } from "../chatClient"; +import { saveScheduledActions, scheduledActions } from "../helpers/scheduledActions"; import { AddressInfo } from "net"; import { Socket } from "net"; @@ -19,6 +20,7 @@ let server: Server; export { listen, + // TODO: use intermediate class to handle socket messages sockets } @@ -67,9 +69,8 @@ function onConnection(socket: WebSocket, req: IncomingMessage) { async function onMessage(msg: string, socket: WebSocket) { const data = JSON.parse(msg); - // broadcast message if (!data.actions || data.actions.length === 0) { - sockets.filter(s => s !== socket).forEach(s => s.send(msg)); + broadcast(msg, socket); return; } @@ -78,7 +79,7 @@ async function onMessage(msg: string, socket: WebSocket) { await handleClientAction(action); } else { scheduledActions.push(action); - scheduledActions.sort((a, b) => a.scheduledAt - b.scheduledAt); + scheduledActions.sort((a: any, b: any) => a.scheduledAt - b.scheduledAt); saveScheduledActions(); } }