From 931cc57b1b8fdf2fccf9ce0f7c802f016e42d250 Mon Sep 17 00:00:00 2001 From: alexbcberio Date: Thu, 6 Jan 2022 18:15:56 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20most=20of=20the=20linting?= =?UTF-8?q?=20error=20and=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatClient/clientActions/addVip/index.ts | 32 +-- .../chatClient/clientActions/hasVip/index.ts | 55 ++++- src/backend/chatClient/clientActions/index.ts | 15 +- .../clientActions/removeVip/index.ts | 33 +-- .../chatClient/clientActions/say/index.ts | 41 +++- .../chatClient/clientActions/timeout/index.ts | 25 ++- .../chatClient/commands/createReward.ts | 58 ++--- src/backend/chatClient/index.ts | 190 ++++++++-------- src/backend/helpers/miniDb.ts | 177 ++++++++------- src/backend/helpers/tokenData.ts | 80 +++---- src/backend/helpers/twitch.ts | 159 +++++++------- src/backend/helpers/webServer.ts | 170 +++++++------- .../pubSubClient/actions/highlightMessage.ts | 47 ++-- .../pubSubClient/actions/russianRoulette.ts | 82 +++---- src/backend/pubSubClient/actions/stealVip.ts | 93 ++++---- .../pubSubClient/actions/timeoutFriend.ts | 49 +++-- src/backend/pubSubClient/index.ts | 207 +++++++++--------- src/interfaces/actions/Action.ts | 11 +- 18 files changed, 829 insertions(+), 695 deletions(-) diff --git a/src/backend/chatClient/clientActions/addVip/index.ts b/src/backend/chatClient/clientActions/addVip/index.ts index f5cec51..b8872f8 100644 --- a/src/backend/chatClient/clientActions/addVip/index.ts +++ b/src/backend/chatClient/clientActions/addVip/index.ts @@ -1,22 +1,28 @@ +import { hasVip, say } from ".."; + import { chatClient } from "../.."; -import { say } from ".."; async function addVip( - channel: string, - username: string, - message?: string + channel: string, + username: string, + message?: string ): Promise { - try { - await chatClient.addVip(channel, username); - } catch (e) { - return false; - } + username = username.toLowerCase(); + if (!(await hasVip(channel, username))) { + return false; + } - if (message) { - await say(channel, message); - } + try { + await chatClient.addVip(channel, username); + } catch (e) { + return false; + } - return true; + if (message) { + await say(channel, message); + } + + return true; } export { addVip }; diff --git a/src/backend/chatClient/clientActions/hasVip/index.ts b/src/backend/chatClient/clientActions/hasVip/index.ts index b070f0e..5a2ca43 100644 --- a/src/backend/chatClient/clientActions/hasVip/index.ts +++ b/src/backend/chatClient/clientActions/hasVip/index.ts @@ -2,23 +2,54 @@ import { chatClient } from "../.."; type CacheType = Record>; const cache: CacheType = {}; +const cacheKeepTime = 2.5e3; + +interface ChannelFetching { + channel: string; + promise: Promise; +} + +const channelsFetching: Array = []; + +async function fetchVips(channel: string): Promise { + const alreadyChecking = channelsFetching.find((c) => c.channel === channel); + + if (alreadyChecking) { + await alreadyChecking.promise; + } else { + const promise = new Promise((res) => { + chatClient.getVips(channel).then((vips) => { + cache[channel] = vips.map((u) => u.toLowerCase()); + + res(); + }); + }); + + channelsFetching.push({ channel, promise }); + + // eslint-disable-next-line no-magic-numbers + const addedIdx = channelsFetching.length - 1; + + await promise; + + channelsFetching.splice(addedIdx); + + setTimeout(() => { + delete cache[channel]; + }, cacheKeepTime); + } +} async function hasVip(channel: string, username: string): Promise { - if (!username) { - return false; - } + username = username.toLowerCase(); - if (!cache[channel]) { - cache[channel] = await chatClient.getVips(channel); + if (!cache[channel]) { + await fetchVips(channel); + } - setTimeout(() => { - delete cache[channel]; - }, 2500); - } + const vips = cache[channel]; - const vips = cache[channel]; - - return vips.includes(username); + return vips.includes(username); } export { hasVip }; diff --git a/src/backend/chatClient/clientActions/index.ts b/src/backend/chatClient/clientActions/index.ts index fd337e9..79428bc 100644 --- a/src/backend/chatClient/clientActions/index.ts +++ b/src/backend/chatClient/clientActions/index.ts @@ -1,7 +1,18 @@ +import { say, sayError, sayInfo, saySuccess, sayWarn } from "./say"; + import { addVip } from "./addVip"; import { hasVip } from "./hasVip"; import { removeVip } from "./removeVip"; -import { say } from "./say"; import { timeout } from "./timeout"; -export { say, timeout, addVip, removeVip, hasVip }; +export { + addVip, + hasVip, + removeVip, + say, + sayError, + sayInfo, + saySuccess, + sayWarn, + timeout, +}; diff --git a/src/backend/chatClient/clientActions/removeVip/index.ts b/src/backend/chatClient/clientActions/removeVip/index.ts index 5780ce9..97a06ef 100644 --- a/src/backend/chatClient/clientActions/removeVip/index.ts +++ b/src/backend/chatClient/clientActions/removeVip/index.ts @@ -1,22 +1,29 @@ +import { hasVip, say } from ".."; + import { chatClient } from "../.."; -import { say } from ".."; async function removeVip( - channel: string, - username: string, - message?: string + channel: string, + username: string, + message?: string ): Promise { - try { - await chatClient.removeVip(channel, username); - } catch (e) { - return false; - } + username = username.toLowerCase(); - if (message) { - await say(channel, message); - } + if (await hasVip(channel, username)) { + return false; + } - return true; + try { + await chatClient.removeVip(channel, username); + } catch (e) { + return false; + } + + if (message) { + await say(channel, message); + } + + return true; } export { removeVip }; diff --git a/src/backend/chatClient/clientActions/say/index.ts b/src/backend/chatClient/clientActions/say/index.ts index b74c0f8..3cff68a 100644 --- a/src/backend/chatClient/clientActions/say/index.ts +++ b/src/backend/chatClient/clientActions/say/index.ts @@ -1,19 +1,36 @@ import { chatClient } from "../.."; -const maxMessageLength = 500; - async function say(channel: string, message: string): Promise { - // message = `MrDestructoid ${message}`; + const maxMessageLength = 500; + // message = `MrDestructoid ${message}`; - if (message.length > 500) { - const suffix = "..."; - message = `${message.substring( - 0, - maxMessageLength - suffix.length - )}${suffix}`; - } + if (message.length > maxMessageLength) { + const startIndex = 0; + const suffix = "..."; - await chatClient.say(channel, message); + message = `${message.substring( + startIndex, + maxMessageLength - suffix.length + )}${suffix}`; + } + + await chatClient.say(channel, message); } -export { say }; +async function sayError(channel: string, message: string): Promise { + await say(channel, `[ERROR] ${message}`); +} + +async function sayWarn(channel: string, message: string): Promise { + await say(channel, `[WARN] ${message}`); +} + +async function sayInfo(channel: string, message: string) { + await say(channel, `[INFO] ${message}`); +} + +async function saySuccess(channel: string, message: string): Promise { + await say(channel, `[SUCCESS] ${message}`); +} + +export { say, sayError, sayWarn, sayInfo, saySuccess }; diff --git a/src/backend/chatClient/clientActions/timeout/index.ts b/src/backend/chatClient/clientActions/timeout/index.ts index da76ea4..1e2e55e 100644 --- a/src/backend/chatClient/clientActions/timeout/index.ts +++ b/src/backend/chatClient/clientActions/timeout/index.ts @@ -1,21 +1,22 @@ import { chatClient } from "../.."; -// timeouts a user in a channel +const defaultTime = 60; + async function timeout( - channel: string, - username: string, - time?: number, - reason?: string + channel: string, + username: string, + time?: number, + reason?: string ): Promise { - if (!time) { - time = 60; - } + if (!time) { + time = defaultTime; + } - if (!reason) { - reason = ""; - } + if (!reason) { + reason = ""; + } - await chatClient.timeout(channel, username, time, reason); + await chatClient.timeout(channel, username, time, reason); } export { timeout }; diff --git a/src/backend/chatClient/commands/createReward.ts b/src/backend/chatClient/commands/createReward.ts index 15930a4..ab4c5d0 100644 --- a/src/backend/chatClient/commands/createReward.ts +++ b/src/backend/chatClient/commands/createReward.ts @@ -1,42 +1,42 @@ -import { LOG_PREFIX, say } from ".."; +import { sayError, saySuccess } from "../clientActions"; +import { LOG_PREFIX } from ".."; import { TwitchPrivateMessage } from "@twurple/chat/lib/commands/TwitchPrivateMessage"; import { createReward as createChannelPointsReward } from "../../helpers/twitch"; async function createReward( - channel: string, - user: string, - message: string, - msg: TwitchPrivateMessage + channel: string, + _user: string, + message: string, + msg: TwitchPrivateMessage ): Promise { - const args = message.split(" "); + const args = message.split(" "); - const title = args.shift(); - const cost = Math.max(1, parseInt(args.shift() ?? "0")); + const title = args.shift(); - if (!title || !cost) { - await say( - channel, - "No se ha especificado el nombre de la recompensa o costo" - ); - return; - } + if (!title) { + await sayError(channel, "Debes indicar el título de la recompensa"); + return; + } - try { - await createChannelPointsReward(msg.channelId as string, { - title, - cost - }); + const minRewardPrice = 1; + const cost = Math.max(minRewardPrice, parseInt(args.shift() ?? "0")); - say( - channel, - `✅ Creada recompensa de canal "${title}" con un costo de ${cost}` - ); - } catch (e) { - if (e instanceof Error) { - console.log(`${LOG_PREFIX}${e.message}`); - } - } + try { + await createChannelPointsReward(msg.channelId as string, { + title, + cost, + }); + + saySuccess( + channel, + `Creada recompensa de canal "${title}" con un costo de ${cost}` + ); + } catch (e) { + if (e instanceof Error) { + console.log(`${LOG_PREFIX}${e.message}`); + } + } } export { createReward }; diff --git a/src/backend/chatClient/index.ts b/src/backend/chatClient/index.ts index fbc0bbd..4cb1415 100644 --- a/src/backend/chatClient/index.ts +++ b/src/backend/chatClient/index.ts @@ -12,107 +12,115 @@ import { start } from "../helpers/miniDb"; let chatClient: ChatClient; -export { chatClient, connect, handleClientAction, say, LOG_PREFIX }; - const LOG_PREFIX = "[ChatClient] "; -async function connect(channels: Array): Promise { - const authProvider = await getAuthProvider(); +function onConnect(): Promise { + console.log(`${LOG_PREFIX}Connected`); - if (chatClient && (chatClient.isConnecting || chatClient.isConnected)) { - return; - } + start(); - chatClient = new ChatClient({ - authProvider, - channels - }); - - chatClient.onConnect(onConnect); - - chatClient.onDisconnect((e: any) => { - console.log(`${LOG_PREFIX}Disconnected ${e.message}`); - }); - - chatClient.onNoPermission((channel, message) => { - console.log(`${LOG_PREFIX}No permission on ${channel}: ${message}`); - }); - - chatClient.onMessage(onMessage); - - await chatClient.connect(); -} - -async function onConnect(): Promise { - console.log(`${LOG_PREFIX}Connected`); - - start(); -} - -async function handleClientAction(action: Action): Promise { - const [channel, username] = await Promise.all([ - getUsernameFromId(parseInt(action.channelId)), - getUsernameFromId(parseInt(action.userId)) - ]); - - if (!channel || !username) { - console.log(`${[LOG_PREFIX]}ChannelId or userId could not be solved`); - return; - } - - switch (action.type) { - case ActionType.Say: - say(channel, action.data.message); - break; - case ActionType.Timeout: - try { - await timeout(channel, username, action.data.time, action.data.reason); - } catch (e) { - // user cannot be timed out - } - break; - case ActionType.Broadcast: - broadcast(action.data.message); - break; - case ActionType.AddVip: - await addVip(channel, username, `Otorgado VIP a @${username}`); - break; - case ActionType.RemoveVip: - await removeVip(channel, username, `Eliminado VIP de @${username}`); - break; - default: - console.log(`${[LOG_PREFIX]}Couldn't handle action:`, action); - } + return Promise.resolve(); } const commandPrefix = "!"; async function onMessage( - channel: string, - user: string, - message: string, - msg: TwitchPrivateMessage + channel: string, + user: string, + message: string, + msg: TwitchPrivateMessage ): Promise { - if (msg.userInfo.isBroadcaster && message.startsWith(commandPrefix)) { - message = message.substring(commandPrefix.length); + if (msg.userInfo.isBroadcaster && message.startsWith(commandPrefix)) { + message = message.substring(commandPrefix.length); - const args = message.split(" "); - const commandName = args.shift(); + const args = message.split(" "); + const commandName = args.shift(); - switch (commandName) { - case ChatCommands.Commands: - await say( - channel, - `Comandos disponibles: "${Object.values(ChatCommands).join('", "')}"` - ); - break; - case ChatCommands.CreateReward: - await createReward(channel, user, args.join(" "), msg); - break; - default: - console.log( - `${LOG_PREFIX}Command ${commandPrefix}${commandName} not handled` - ); - } - } + switch (commandName) { + case ChatCommands.Commands: + await say( + channel, + `Comandos disponibles: "${Object.values(ChatCommands).join('", "')}"` + ); + break; + case ChatCommands.CreateReward: + await createReward(channel, user, args.join(" "), msg); + break; + default: + console.log( + `${LOG_PREFIX}Command ${commandPrefix}${commandName} not handled` + ); + } + } } + +async function connect(channels: Array): Promise { + const authProvider = await getAuthProvider(); + + if (chatClient && (chatClient.isConnecting || chatClient.isConnected)) { + return; + } + + chatClient = new ChatClient({ + authProvider, + channels, + webSocket: true, + }); + + chatClient.onConnect(onConnect); + + chatClient.onDisconnect((manually, reason) => { + if (manually) { + console.log(`${LOG_PREFIX}Disconnected manually`); + return; + } + + console.log(`${LOG_PREFIX}Disconnected ${reason}`); + }); + + chatClient.onNoPermission((channel, message) => { + console.log(`${LOG_PREFIX}No permission on ${channel}: ${message}`); + }); + + chatClient.onMessage(onMessage); + + await chatClient.connect(); +} + +async function handleClientAction(action: Action): Promise { + const [channel, username] = await Promise.all([ + getUsernameFromId(parseInt(action.channelId)), + getUsernameFromId(parseInt(action.userId)), + ]); + + if (!channel || !username) { + console.log(`${[LOG_PREFIX]}ChannelId or userId could not be solved`); + return; + } + + switch (action.type) { + case ActionType.Say: + say(channel, action.data.message); + break; + case ActionType.Timeout: + try { + await timeout(channel, username, action.data.time, action.data.reason); + } catch (e) { + // user cannot be timed out + } + break; + case ActionType.Broadcast: + broadcast(action.data.message); + break; + case ActionType.AddVip: + await addVip(channel, username, `Otorgado VIP a @${username}`); + break; + case ActionType.RemoveVip: + await removeVip(channel, username, `Eliminado VIP de @${username}`); + break; + default: + console.log(`${[LOG_PREFIX]}Couldn't handle action:`, action); + } +} + +export { chatClient, connect, handleClientAction, say, LOG_PREFIX }; diff --git a/src/backend/helpers/miniDb.ts b/src/backend/helpers/miniDb.ts index bb849e3..aa14b6f 100644 --- a/src/backend/helpers/miniDb.ts +++ b/src/backend/helpers/miniDb.ts @@ -1,3 +1,4 @@ +import { Action } from "../../interfaces/actions/Action"; import { promises as fs } from "fs"; import { handleClientAction } from "../chatClient"; import { resolve } from "path"; @@ -12,104 +13,124 @@ const FILES_BASE = resolve(process.cwd(), "./storage"); const SCHEDULED_FILE = resolve(FILES_BASE, "./scheduled.json"); const VIP_USERS_FILE = resolve(FILES_BASE, "./vips.json"); -const scheduledActions: Array = []; +const defaultScheduledTimestamp = 0; + +let scheduledActions: Array = []; + const vipUsers: Record> = {}; let checkingScheduled = false; let scheduledActionsInterval: NodeJS.Timeout; let saveScheduledActionsTimeout: NodeJS.Timeout | null; -// *Check this, not working -async function start(): Promise { - if (scheduledActionsInterval) { - return; - } +function save(): void { + if (saveScheduledActionsTimeout) { + clearTimeout(saveScheduledActionsTimeout); + saveScheduledActionsTimeout = null; + console.log(`${LOG_PREFIX}Removed save timeout.`); + } - let savedActions = []; - try { - savedActions = JSON.parse((await fs.readFile(SCHEDULED_FILE)).toString()); - } catch (e) { - // probably file does not exist - if (e instanceof Error) { - console.log(`${LOG_PREFIX}${e.message}`); - } - } + saveScheduledActionsTimeout = setTimeout(async () => { + await Promise.all([ + fs.writeFile(SCHEDULED_FILE, JSON.stringify(scheduledActions)), + fs.writeFile(VIP_USERS_FILE, JSON.stringify(vipUsers)), + ]); - scheduledActions.push.apply(scheduledActions, savedActions); - scheduledActions.sort((a, b) => a.scheduledAt - b.scheduledAt); - - setTimeout(checkScheduledActions, FIRST_CHECK_TIMEOUT); - scheduledActionsInterval = setInterval(checkScheduledActions, CHECK_INTERVAL); - - try { - const savedVipUsers = JSON.parse( - await (await fs.readFile(VIP_USERS_FILE)).toString() - ); - - for (const key of Object.keys(savedVipUsers)) { - vipUsers[key] = savedVipUsers[key]; - } - } catch (e) { - // probably file does not exist - if (e instanceof Error) { - console.log(`${LOG_PREFIX}${e.message}`); - } - } + console.log(`${LOG_PREFIX}Saved actions.`); + saveScheduledActionsTimeout = null; + }, SAVE_TIMEOUT); } async function checkScheduledActions(): Promise { - if (checkingScheduled) { - return; - } + if (checkingScheduled) { + return; + } - checkingScheduled = true; + checkingScheduled = true; - let hasToSave = false; + let hasToSave = false; - for ( - let i = 0; - i < scheduledActions.length && - scheduledActions[i].scheduledAt <= Date.now(); - i++ - ) { - hasToSave = true; + for ( + let i = 0; + i < scheduledActions.length && + (scheduledActions[i].scheduledAt ?? defaultScheduledTimestamp) <= + Date.now(); + i++ + ) { + hasToSave = true; - const action = scheduledActions.splice(i, 1)[0]; - await handleClientAction(action); - console.log(`${LOG_PREFIX}Executed: ${JSON.stringify(action)}`); - } + const deleteCount = 1; + const deletedActions = scheduledActions.splice(i, deleteCount); + const action = deletedActions.shift(); - if (hasToSave) { - save(); - } + if (action) { + await handleClientAction(action); + } - checkingScheduled = false; + console.log(`${LOG_PREFIX}Executed: ${JSON.stringify(action)}`); + } + + if (hasToSave) { + save(); + } + + checkingScheduled = false; +} + +async function start(): Promise { + if (scheduledActionsInterval) { + return; + } + + let savedActions = []; + try { + savedActions = JSON.parse((await fs.readFile(SCHEDULED_FILE)).toString()); + } catch (e) { + // probably file does not exist + if (e instanceof Error) { + console.log(`${LOG_PREFIX}${e.message}`); + } + } + + scheduledActions = [...scheduledActions, ...savedActions]; + scheduledActions.sort((a, b) => { + if (typeof a.scheduledAt === "undefined") { + a.scheduledAt = defaultScheduledTimestamp; + } + + if (typeof b.scheduledAt === "undefined") { + b.scheduledAt = defaultScheduledTimestamp; + } + + return a.scheduledAt - b.scheduledAt; + }); + + setTimeout(checkScheduledActions, FIRST_CHECK_TIMEOUT); + // eslint-disable-next-line require-atomic-updates + scheduledActionsInterval = setInterval(checkScheduledActions, CHECK_INTERVAL); + + try { + const savedVipUsers = JSON.parse( + await (await fs.readFile(VIP_USERS_FILE)).toString() + ); + + for (const key of Object.keys(savedVipUsers)) { + vipUsers[key] = savedVipUsers[key]; + } + } catch (e) { + // probably file does not exist + if (e instanceof Error) { + console.log(`${LOG_PREFIX}${e.message}`); + } + } } async function createSaveDirectory() { - try { - await fs.stat(FILES_BASE); - } catch (e) { - await fs.mkdir(FILES_BASE); - } -} - -function save(): void { - if (saveScheduledActionsTimeout) { - clearTimeout(saveScheduledActionsTimeout); - saveScheduledActionsTimeout = null; - console.log(`${LOG_PREFIX}Removed save timeout.`); - } - - saveScheduledActionsTimeout = setTimeout(async () => { - await Promise.all([ - fs.writeFile(SCHEDULED_FILE, JSON.stringify(scheduledActions)), - fs.writeFile(VIP_USERS_FILE, JSON.stringify(vipUsers)) - ]); - - console.log(`${LOG_PREFIX}Saved actions.`); - saveScheduledActionsTimeout = null; - }, SAVE_TIMEOUT); + try { + await fs.stat(FILES_BASE); + } catch (e) { + await fs.mkdir(FILES_BASE); + } } createSaveDirectory(); diff --git a/src/backend/helpers/tokenData.ts b/src/backend/helpers/tokenData.ts index 998e55b..1826748 100644 --- a/src/backend/helpers/tokenData.ts +++ b/src/backend/helpers/tokenData.ts @@ -5,45 +5,51 @@ import { resolve } from "path"; const TOKENS_FILE = "tokens.json"; const LOG_PREFIX = "[TokenData] "; -export { getTokenData, saveTokenData }; - function getTokenDataFilePath(): string { - return resolve(process.cwd(), TOKENS_FILE); -} - -async function getTokenData(): Promise { - const tokenDataFilePath = getTokenDataFilePath(); - let buffer: Buffer; - - try { - buffer = await fs.readFile(tokenDataFilePath); - } catch (e) { - console.error( - `${LOG_PREFIX}${TOKENS_FILE} not found on ${tokenDataFilePath}.` - ); - process.exit(1); - } - - const tokenData = await JSON.parse(buffer.toString()); - - checkTokenData(tokenData); - - return tokenData; -} - -async function saveTokenData(tokenData: AccessToken): Promise { - const tokenDataFilePath = getTokenDataFilePath(); - const jsonTokenData = JSON.stringify(tokenData); - - await fs.writeFile(tokenDataFilePath, jsonTokenData); - console.log(`${LOG_PREFIX}Token data saved`); + return resolve(process.cwd(), TOKENS_FILE); } function checkTokenData(tokenData: AccessToken): void { - if (!tokenData.accessToken || !tokenData.refreshToken) { - console.error( - `${LOG_PREFIX}Missing refresh_token or access_token in ${TOKENS_FILE}.` - ); - process.exit(1); - } + if (!tokenData.accessToken || !tokenData.refreshToken) { + console.error( + `${LOG_PREFIX}Missing refreshToken or accessToken in ${TOKENS_FILE}.` + ); + + const exitCode = 1; + + process.exit(exitCode); + } } + +async function getTokenData(): Promise { + const tokenDataFilePath = getTokenDataFilePath(); + let buffer: Buffer; + + try { + buffer = await fs.readFile(tokenDataFilePath); + } catch (e) { + console.error( + `${LOG_PREFIX}${TOKENS_FILE} not found on ${tokenDataFilePath}.` + ); + + const exitCode = 1; + + process.exit(exitCode); + } + + const tokenData = await JSON.parse(buffer.toString()); + + checkTokenData(tokenData); + + return tokenData; +} + +async function saveTokenData(tokenData: AccessToken): Promise { + const tokenDataFilePath = getTokenDataFilePath(); + const jsonTokenData = JSON.stringify(tokenData); + + await fs.writeFile(tokenDataFilePath, jsonTokenData); + console.log(`${LOG_PREFIX}Token data saved`); +} + +export { getTokenData, saveTokenData }; diff --git a/src/backend/helpers/twitch.ts b/src/backend/helpers/twitch.ts index 6e8cd92..97c822d 100644 --- a/src/backend/helpers/twitch.ts +++ b/src/backend/helpers/twitch.ts @@ -1,8 +1,8 @@ import { AccessToken, RefreshingAuthProvider } from "@twurple/auth"; import { - ApiClient, - HelixCreateCustomRewardData, - UserIdResolvable + ApiClient, + HelixCreateCustomRewardData, + UserIdResolvable, } from "@twurple/api"; import { getTokenData, saveTokenData } from "./tokenData"; @@ -13,115 +13,118 @@ const LOG_PREFIX = "[Twitch] "; let refreshAuthProvider: RefreshingAuthProvider; function getClientCredentials(): ClientCredentials { - if (!process.env.TWITCH_CLIENT_ID || !process.env.TWITCH_CLIENT_SECRET) { - console.error( - `${LOG_PREFIX}Missing environment parameters TWITCH_CLIENT_ID or TWITCH_CLIENT_SECRET` - ); - process.exit(1); - } + if (!process.env.TWITCH_CLIENT_ID || !process.env.TWITCH_CLIENT_SECRET) { + console.error( + `${LOG_PREFIX}Missing environment parameters TWITCH_CLIENT_ID or TWITCH_CLIENT_SECRET` + ); - return { - clientId: process.env.TWITCH_CLIENT_ID, - clientSecret: process.env.TWITCH_CLIENT_SECRET - }; -} + const exitCode = 1; -async function getAuthProvider(): Promise { - if (refreshAuthProvider) { - return refreshAuthProvider; - } + process.exit(exitCode); + } - let tokenData = await getTokenData(); - - const credentials = getClientCredentials(); - - refreshAuthProvider = new RefreshingAuthProvider( - { - clientId: credentials.clientId, - clientSecret: credentials.clientSecret, - onRefresh - }, - tokenData - ); - - return refreshAuthProvider; + return { + clientId: process.env.TWITCH_CLIENT_ID, + clientSecret: process.env.TWITCH_CLIENT_SECRET, + }; } async function onRefresh(refreshData: AccessToken): Promise { - console.log(`${LOG_PREFIX}Tokens refreshed`); + console.log(`${LOG_PREFIX}Tokens refreshed`); - await saveTokenData(refreshData); + await saveTokenData(refreshData); +} + +async function getAuthProvider(): Promise { + if (refreshAuthProvider) { + return refreshAuthProvider; + } + + const tokenData = await getTokenData(); + const credentials = getClientCredentials(); + + // eslint-disable-next-line require-atomic-updates + refreshAuthProvider = new RefreshingAuthProvider( + { + clientId: credentials.clientId, + clientSecret: credentials.clientSecret, + onRefresh, + }, + tokenData + ); + + return refreshAuthProvider; } async function getApiClient(): Promise { - const authProvider = await getAuthProvider(); + const authProvider = await getAuthProvider(); - return await new ApiClient({ authProvider }); + return new ApiClient({ authProvider }); } async function getUsernameFromId(userId: number): Promise { - const apiClient = await getApiClient(); - const user = await apiClient.users.getUserById(userId); + const apiClient = await getApiClient(); + const user = await apiClient.users.getUserById(userId); - if (!user) { - return null; - } + if (!user) { + return null; + } - return user.displayName; + return user.displayName; } async function createReward( - userId: UserIdResolvable, - data: HelixCreateCustomRewardData + userId: UserIdResolvable, + data: HelixCreateCustomRewardData ) { - const apiClient = await getApiClient(); + const apiClient = await getApiClient(); - await apiClient.channelPoints.createCustomReward(userId, data); + await apiClient.channelPoints.createCustomReward(userId, data); } async function completeRewards( - channel: UserIdResolvable, - rewardId: string, - redemptionIds: Array | string + channel: UserIdResolvable, + rewardId: string, + redemptionIds: Array | string ) { - if (!Array.isArray(redemptionIds)) { - redemptionIds = [redemptionIds]; - } + if (!Array.isArray(redemptionIds)) { + redemptionIds = [redemptionIds]; + } - const apiClient = await getApiClient(); + const apiClient = await getApiClient(); - await apiClient.channelPoints.updateRedemptionStatusByIds( - channel, - rewardId, - redemptionIds, - "FULFILLED" - ); + await apiClient.channelPoints.updateRedemptionStatusByIds( + channel, + rewardId, + redemptionIds, + "FULFILLED" + ); } async function cancelRewards( - channel: UserIdResolvable, - rewardId: string, - redemptionIds: Array | string + channel: UserIdResolvable, + rewardId: string, + redemptionIds: Array | string ) { - if (!Array.isArray(redemptionIds)) { - redemptionIds = [redemptionIds]; - } + if (!Array.isArray(redemptionIds)) { + redemptionIds = [redemptionIds]; + } - const apiClient = await getApiClient(); + const apiClient = await getApiClient(); - await apiClient.channelPoints.updateRedemptionStatusByIds( - channel, - rewardId, - redemptionIds, - "CANCELED" - ); + await apiClient.channelPoints.updateRedemptionStatusByIds( + channel, + rewardId, + redemptionIds, + "CANCELED" + ); } export { - getAuthProvider, - getApiClient, - getUsernameFromId, - completeRewards, - cancelRewards, - createReward + getAuthProvider, + getApiClient, + getUsernameFromId, + completeRewards, + cancelRewards, + createReward, }; diff --git a/src/backend/helpers/webServer.ts b/src/backend/helpers/webServer.ts index b5afd36..0b568b2 100644 --- a/src/backend/helpers/webServer.ts +++ b/src/backend/helpers/webServer.ts @@ -1,9 +1,8 @@ +import { AddressInfo, Socket } from "net"; import { IncomingMessage, Server } from "http"; import { save, scheduledActions } from "./miniDb"; import { Action } from "../../interfaces/actions/Action"; -import { AddressInfo } from "net"; -import { Socket } from "net"; import WebSocket from "ws"; import express from "express"; import { handleClientAction } from "../chatClient"; @@ -17,98 +16,103 @@ const app = express(); const sockets: Array = []; const wsServer = new WebSocket.Server({ - noServer: true + noServer: true, }); let server: Server; -export { listen, broadcast }; +function broadcast(msg: string, socket?: WebSocket) { + const filteredSockets = socket + ? sockets.filter((s) => s !== socket) + : sockets; + + filteredSockets.forEach((s) => s.send(msg)); +} + +async function onMessage(this: WebSocket, msg: string) { + const data = JSON.parse(msg); + + if (!data.actions) { + broadcast(msg, this); + return; + } + + const actions: Array = data.actions; + + for (const action of actions) { + if (!action.scheduledAt) { + await handleClientAction(action); + } else { + scheduledActions.push(action); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + scheduledActions.sort((a: any, b: any) => a.scheduledAt - b.scheduledAt); + save(); + } + } + + console.log( + `${LOG_PREFIX_WS}Received message with ${data.actions.length} actions:`, + data + ); +} + +function onClose(this: WebSocket) { + const socketIdx = sockets.indexOf(this); + const deleteCount = 1; + + sockets.splice(socketIdx, deleteCount); + console.log(`${LOG_PREFIX_WS}Connection closed`); +} + +function onListening() { + console.log( + `${LOG_PREFIX_HTTP}Listening on port ${ + (server.address() as AddressInfo).port + }` + ); +} + +function onUpgrade(req: IncomingMessage, socket: Socket, head: Buffer) { + wsServer.handleUpgrade(req, socket, head, (socket) => { + wsServer.emit("connection", socket, req); + }); +} + +function onConnection(socket: WebSocket, req: IncomingMessage) { + console.log( + `${LOG_PREFIX_WS}${req.socket.remoteAddress} New connection established` + ); + sockets.push(socket); + socket.send( + JSON.stringify({ + env: isDevelopment ? "dev" : "prod", + }) + ); + + socket.on("message", onMessage); + socket.on("close", onClose); +} wsServer.on("connection", onConnection); app.use(express.static(join(process.cwd(), "client"))); function listen() { - if (server) { - console.log(`${LOG_PREFIX_HTTP}Server is already running`); - return; - } + if (server) { + console.log(`${LOG_PREFIX_HTTP}Server is already running`); + return; + } - server = app.listen(!isDevelopment ? 8080 : 8081, "0.0.0.0"); + let port = 8080; - server.on("listening", onListening); - server.on("upgrade", onUpgrade); + if (isDevelopment) { + port++; + } + + server = app.listen(port, "0.0.0.0"); + + server.on("listening", onListening); + server.on("upgrade", onUpgrade); } -function onListening() { - console.log( - `${LOG_PREFIX_HTTP}Listening on port ${ - (server.address() as AddressInfo).port - }` - ); -} - -function onUpgrade(req: IncomingMessage, socket: Socket, head: Buffer) { - wsServer.handleUpgrade(req, socket, head, socket => { - wsServer.emit("connection", socket, req); - }); -} - -function onConnection(socket: WebSocket, req: IncomingMessage) { - console.log( - `${LOG_PREFIX_WS}${req.socket.remoteAddress} New connection established` - ); - sockets.push(socket); - socket.send( - JSON.stringify({ - env: isDevelopment ? "dev" : "prod" - }) - ); - - socket.on("message", onMessage); - socket.on("close", onClose); -} - -// 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)); -} - -async function onMessage(msg: string) { - // @ts-ignore - const socket = this as WebSocket; - const data = JSON.parse(msg); - - if (!data.actions || data.actions.length === 0) { - broadcast(msg, socket); - return; - } - - const actions: Array = data.actions; - - for (const action of actions) { - if (!action.scheduledAt) { - await handleClientAction(action); - } else { - scheduledActions.push(action); - scheduledActions.sort((a: any, b: any) => a.scheduledAt - b.scheduledAt); - save(); - } - } - - console.log( - `${LOG_PREFIX_WS}Received message with ${data.actions.length} actions:`, - data - ); -} - -function onClose() { - // @ts-ignore - const socket: WebSocket = this as WebSocket; - - const socketIdx = sockets.indexOf(socket); - sockets.splice(socketIdx, 1); - console.log(`${LOG_PREFIX_WS}Connection closed`); -} +export { listen, broadcast }; diff --git a/src/backend/pubSubClient/actions/highlightMessage.ts b/src/backend/pubSubClient/actions/highlightMessage.ts index 722ab21..0465d70 100644 --- a/src/backend/pubSubClient/actions/highlightMessage.ts +++ b/src/backend/pubSubClient/actions/highlightMessage.ts @@ -4,39 +4,40 @@ import { getUsernameFromId } from "../../helpers/twitch"; import { timeout } from "../../chatClient/clientActions"; async function highlightMessage( - msg: RedemptionMessage + msg: RedemptionMessage ): Promise { - if (!msg.message) { - console.log(`${LOG_PREFIX}Redemption has no message`); + if (!msg.message) { + console.log(`${LOG_PREFIX}Redemption has no message`); - return; - } + return; + } - const urlRegex = - /(https?:\/\/)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&/=]*)/; + const urlRegex = + /(https?:\/\/)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)/; - if (urlRegex.test(msg.message)) { - console.log(`${LOG_PREFIX}Message contains a url`); - const channel = await getUsernameFromId(parseInt(msg.channelId)); + if (urlRegex.test(msg.message)) { + console.log(`${LOG_PREFIX}Message contains a url`); + const channel = await getUsernameFromId(parseInt(msg.channelId)); - if (!channel) { - console.log(`${LOG_PREFIX}No channel found`); + if (!channel) { + console.log(`${LOG_PREFIX}No channel found`); - return; - } + return; + } - try { - const reason = "No se permite enviar enlaces en mensajes destacados"; + try { + const reason = "No se permite enviar enlaces en mensajes destacados"; + const timeoutSeconds = 10; - await timeout(channel, msg.userDisplayName, 10, reason); - } catch (e) { - // user probably cannot be timed out - } + await timeout(channel, msg.userDisplayName, timeoutSeconds, reason); + } catch (e) { + // user probably cannot be timed out + } - return; - } + return; + } - return msg; + return msg; } export { highlightMessage }; diff --git a/src/backend/pubSubClient/actions/russianRoulette.ts b/src/backend/pubSubClient/actions/russianRoulette.ts index b7f7774..640efa6 100644 --- a/src/backend/pubSubClient/actions/russianRoulette.ts +++ b/src/backend/pubSubClient/actions/russianRoulette.ts @@ -12,56 +12,60 @@ const timeoutSeconds = 60; const maxSafeShots = 5; async function russianRoulette( - msg: RedemptionMessage + msg: RedemptionMessage ): Promise { - const { channelId, userDisplayName } = msg; - const channel = await getUsernameFromId(parseInt(channelId)); + const { channelId, userDisplayName } = msg; + const channel = await getUsernameFromId(parseInt(channelId)); - if (!channel) { - console.log(`${LOG_PREFIX}No channel found`); + if (!channel) { + console.log(`${LOG_PREFIX}No channel found`); - return; - } + return; + } - if (!gunsSafeShots[channelId]) { - gunsSafeShots[channelId] = maxSafeShots; - } + if (!gunsSafeShots[channelId]) { + gunsSafeShots[channelId] = maxSafeShots; + } - const win = - gunsSafeShots[channelId] > 0 && - randomInt(gunsSafeShots[channelId]-- + 1) !== 0; + const noShots = 0; - if (gunsSafeShots[channelId] < 0 || !win) { - gunsSafeShots[channelId] = maxSafeShots; - } + const win = + gunsSafeShots[channelId] > noShots && + // eslint-disable-next-line no-magic-numbers + randomInt(gunsSafeShots[channelId]-- + 1) !== 0; - msg.message = win ? "" : "got shot"; + if (gunsSafeShots[channelId] < noShots || !win) { + gunsSafeShots[channelId] = maxSafeShots; + } - const promises: Array> = []; + // eslint-disable-next-line require-atomic-updates + msg.message = win ? "" : "got shot"; - if (!win) { - promises.push( - timeout(channel, userDisplayName, timeoutSeconds, "F en la ruleta") - ); - promises.push( - say( - channel, - `PepeHands ${userDisplayName} no ha sobrevivido para contarlo` - ) - ); - } else { - promises.push(say(channel, `rdCool Clap ${userDisplayName}`)); - } + const promises: Array> = []; - try { - await Promise.allSettled(promises); - } catch (e) { - if (e instanceof Error) { - console.log(`${LOG_PREFIX}${e.message}`); - } - } + if (!win) { + promises.push( + timeout(channel, userDisplayName, timeoutSeconds, "F en la ruleta") + ); + promises.push( + say( + channel, + `PepeHands ${userDisplayName} no ha sobrevivido para contarlo` + ) + ); + } else { + promises.push(say(channel, `rdCool Clap ${userDisplayName}`)); + } - return msg; + try { + await Promise.allSettled(promises); + } catch (e) { + if (e instanceof Error) { + console.log(`${LOG_PREFIX}${e.message}`); + } + } + + return msg; } export { russianRoulette }; diff --git a/src/backend/pubSubClient/actions/stealVip.ts b/src/backend/pubSubClient/actions/stealVip.ts index 9ecc8b8..6970b6a 100644 --- a/src/backend/pubSubClient/actions/stealVip.ts +++ b/src/backend/pubSubClient/actions/stealVip.ts @@ -7,70 +7,73 @@ import { getUsernameFromId } from "../../helpers/twitch"; // remove vip from a user to grant it to yourself async function stealVip( - msg: RedemptionMessage + msg: RedemptionMessage ): Promise { - if (!msg.message) { - console.log(`${LOG_PREFIX}Redemption has no message`); + if (!msg.message) { + console.log(`${LOG_PREFIX}Redemption has no message`); - return; - } + return; + } - const channelId = parseInt(msg.channelId); - const channel = await getUsernameFromId(channelId); + const channelId = parseInt(msg.channelId); + const channel = await getUsernameFromId(channelId); - if (!channel) { - console.log(`${LOG_PREFIX}No channel found`); + if (!channel) { + console.log(`${LOG_PREFIX}No channel found`); - return; - } + return; + } - const addVipUser = msg.userDisplayName; - const removeVipUser = msg.message.toLowerCase(); - const channelVips = vipUsers[channelId]; + const addVipUser = msg.userDisplayName; + const removeVipUser = msg.message.toLowerCase(); + const channelVips = vipUsers[channelId]; - if (!channelVips.find(u => u.toLowerCase() === removeVipUser)) { - const message = - channelVips.length === 0 - ? "No hay nadie a quien puedas robar el VIP" - : `Solo puedes robar el VIP de: "${channelVips.sort().join('", "')}"`; - await say(channel, message); + if (!channelVips.find((u) => u.toLowerCase() === removeVipUser)) { + const message = + // eslint-disable-next-line no-magic-numbers + channelVips.length === 0 + ? "No hay nadie a quien puedas robar el VIP" + : `Solo puedes robar el VIP de: "${channelVips.sort().join('", "')}"`; + await say(channel, message); - return; - } + return; + } - if (channelVips.includes(addVipUser) || (await hasVip(channel, addVipUser))) { - console.log(`${LOG_PREFIX}@${addVipUser} is already VIP`); + if (channelVips.includes(addVipUser) || (await hasVip(channel, addVipUser))) { + console.log(`${LOG_PREFIX}@${addVipUser} is already VIP`); - return; - } + return; + } - const removed = await removeVip(channel, removeVipUser); + const removed = await removeVip(channel, removeVipUser); - if (!removed && (await hasVip(channel, removeVipUser))) { - console.log(`${LOG_PREFIX}Could not remove VIP of @${removeVipUser}`); - return; - } + if (!removed && (await hasVip(channel, removeVipUser))) { + console.log(`${LOG_PREFIX}Could not remove VIP of @${removeVipUser}`); + return; + } - const added = await addVip(channel, addVipUser); + const added = await addVip(channel, addVipUser); - if (!added) { - await addVip(channel, removeVipUser); + if (!added) { + await addVip(channel, removeVipUser); - return; - } + return; + } - const removeIdx = channelVips.findIndex( - u => u.toLowerCase() === removeVipUser - ); + const removeIdx = channelVips.findIndex( + (u) => u.toLowerCase() === removeVipUser + ); - channelVips.splice(removeIdx); - channelVips.push(addVipUser); - save(); + channelVips.splice(removeIdx); + channelVips.push(addVipUser); + save(); - msg.message = `@${addVipUser} ha "tomado prestado" el VIP de @${removeVipUser}`; - await say(channel, msg.message); + // eslint-disable-next-line require-atomic-updates + msg.message = `@${addVipUser} ha "tomado prestado" el VIP de @${removeVipUser}`; - return msg; + await say(channel, msg.message); + + return msg; } export { stealVip }; diff --git a/src/backend/pubSubClient/actions/timeoutFriend.ts b/src/backend/pubSubClient/actions/timeoutFriend.ts index 77d01ea..cb840b2 100644 --- a/src/backend/pubSubClient/actions/timeoutFriend.ts +++ b/src/backend/pubSubClient/actions/timeoutFriend.ts @@ -4,40 +4,41 @@ import { getUsernameFromId } from "../../helpers/twitch"; import { timeout } from "../../chatClient/clientActions"; async function timeoutFriend( - msg: RedemptionMessage + msg: RedemptionMessage ): Promise { - const { message, channelId, userDisplayName } = msg; - if (!msg.message) { - console.log(`${LOG_PREFIX}Redemption has no message`); + const { message, channelId, userDisplayName } = msg; + if (!msg.message) { + console.log(`${LOG_PREFIX}Redemption has no message`); - return; - } + return; + } - const channel = await getUsernameFromId(parseInt(channelId)); + const channel = await getUsernameFromId(parseInt(channelId)); - if (!channel) { - console.log(`${LOG_PREFIX}No channel found`); + if (!channel) { + console.log(`${LOG_PREFIX}No channel found`); - return; - } + return; + } - const time = 60; - const reason = `Timeout dado por @${userDisplayName} con puntos del canal`; + const time = 60; + const reason = `Timeout dado por @${userDisplayName} con puntos del canal`; - try { - await timeout(channel, msg.message, time, reason); + try { + await timeout(channel, msg.message, time, reason); - msg.message = `@${userDisplayName} ha expulsado a @${message} por ${time} segundos`; - } catch (e) { - // user can not be timed out - if (e instanceof Error) { - console.error(`${LOG_PREFIX} ${e.message}`); - } + // eslint-disable-next-line require-atomic-updates + msg.message = `@${userDisplayName} ha expulsado a @${message} por ${time} segundos`; + } catch (e) { + // user can not be timed out + if (e instanceof Error) { + console.error(`${LOG_PREFIX} ${e.message}`); + } - return; - } + return; + } - return msg; + return msg; } export { timeoutFriend }; diff --git a/src/backend/pubSubClient/index.ts b/src/backend/pubSubClient/index.ts index dcc7289..e0244ce 100644 --- a/src/backend/pubSubClient/index.ts +++ b/src/backend/pubSubClient/index.ts @@ -1,8 +1,8 @@ import { PubSubClient, PubSubRedemptionMessage } from "@twurple/pubsub"; import { - cancelRewards, - completeRewards, - getAuthProvider + cancelRewards, + completeRewards, + getAuthProvider, } from "../helpers/twitch"; import { RedemptionIds } from "../../enums/Redemptions"; @@ -20,107 +20,116 @@ import { timeoutFriend } from "./actions/timeoutFriend"; const LOG_PREFIX = "[PubSub] "; -async function registerUserListener(user: UserIdResolvable) { - const pubSubClient = new PubSubClient(); - const userId = await pubSubClient.registerUserListener( - await getAuthProvider(), - user - ); - /*const listener = */ await pubSubClient.onRedemption(userId, onRedemption); +async function onRedemption(message: PubSubRedemptionMessage) { + console.log( + `${LOG_PREFIX}Reward: "${message.rewardTitle}" (${message.rewardId}) redeemed by ${message.userDisplayName}` + ); - console.log(`${LOG_PREFIX}Connected & registered`); + const raw = message[rawDataSymbol]; + + const msg: RedemptionMessage = { + id: message.id, + channelId: message.channelId, + rewardId: message.rewardId, + rewardName: message.rewardTitle, + rewardImage: message.rewardImage + ? message.rewardImage.url_4x + : "https://static-cdn.jtvnw.net/custom-reward-images/default-4.png", + message: message.message, + userId: message.userId, + userDisplayName: message.userDisplayName, + backgroundColor: raw.data.redemption.reward.background_color, + }; + + let handledMessage: RedemptionMessage | undefined; + + try { + // TODO: extract to function + // Returns the executor function, then call it inside a try/catch + switch (msg.rewardId) { + case RedemptionIds.GetVip: + handledMessage = await getVip(msg); + break; + case RedemptionIds.Hidrate: + handledMessage = await hidrate(msg); + break; + case RedemptionIds.HighlightMessage: + handledMessage = await highlightMessage(msg); + break; + case RedemptionIds.RussianRoulette: + handledMessage = await russianRoulette(msg); + break; + case RedemptionIds.StealVip: + handledMessage = await stealVip(msg); + break; + case RedemptionIds.TimeoutFriend: + handledMessage = await timeoutFriend(msg); + break; + default: + console.log(`${LOG_PREFIX}Unhandled redemption ${msg.rewardId}`); + + handledMessage = msg; + break; + } + } catch (e) { + if (e instanceof Error) { + console.error(`${LOG_PREFIX}${e.message}`); + } + } + + if (handledMessage) { + const rewardEnumValues = Object.values(RedemptionIds); + const rewardIdValueIndex = rewardEnumValues.indexOf( + // @ts-expect-error String is not assignable to... but all keys are strings + handledMessage.rewardId + ); + const rewardName = Object.keys(RedemptionIds)[rewardIdValueIndex]; + + handledMessage.rewardId = rewardName; + + broadcast(JSON.stringify(handledMessage)); + } + + // TODO: improve this check + const keepInQueueRewards = [RedemptionIds.KaraokeTime]; + + // @ts-expect-error String is not assignable to... but all keys are strings + if (keepInQueueRewards.includes(message.rewardId)) { + console.log(`${LOG_PREFIX}Reward kept in queue due to config`); + return; + } + + const completeOrCancelReward = + handledMessage && isProduction ? completeRewards : cancelRewards; + + if (message.rewardIsQueued) { + try { + await completeOrCancelReward( + message.channelId, + message.rewardId, + message.id + ); + console.log( + `${LOG_PREFIX}Reward removed from queue (completed or canceled)` + ); + } catch (e) { + if (e instanceof Error) { + console.log(`${LOG_PREFIX}${e.message}`); + } + } + } } -async function onRedemption(message: PubSubRedemptionMessage) { - console.log( - `${LOG_PREFIX}Reward: "${message.rewardTitle}" (${message.rewardId}) redeemed by ${message.userDisplayName}` - ); +async function registerUserListener(user: UserIdResolvable) { + const pubSubClient = new PubSubClient(); + const userId = await pubSubClient.registerUserListener( + await getAuthProvider(), + user + ); - const raw = message[rawDataSymbol]; + await pubSubClient.onRedemption(userId, onRedemption); - const msg: RedemptionMessage = { - id: message.id, - channelId: message.channelId, - rewardId: message.rewardId, - rewardName: message.rewardTitle, - rewardImage: message.rewardImage - ? message.rewardImage.url_4x - : "https://static-cdn.jtvnw.net/custom-reward-images/default-4.png", - message: message.message, - userId: message.userId, - userDisplayName: message.userDisplayName, - backgroundColor: raw.data.redemption.reward.background_color - }; - - let handledMessage: RedemptionMessage | undefined; - - switch (msg.rewardId) { - case RedemptionIds.RussianRoulette: - handledMessage = await russianRoulette(msg); - break; - case RedemptionIds.TimeoutFriend: - handledMessage = await timeoutFriend(msg); - break; - case RedemptionIds.HighlightMessage: - handledMessage = await highlightMessage(msg); - break; - case RedemptionIds.GetVip: - handledMessage = await getVip(msg); - break; - case RedemptionIds.StealVip: - handledMessage = await stealVip(msg); - break; - case RedemptionIds.Hidrate: - handledMessage = await hidrate(msg); - break; - default: - console.log(`${LOG_PREFIX}Unhandled redemption ${msg.rewardId}`); - - handledMessage = msg; - break; - } - - if (handledMessage) { - const rewardEnumValues = Object.values(RedemptionIds); - const rewardIdValueIndex = rewardEnumValues.indexOf( - // @ts-expect-error String is not assignable to... but all keys are strings - handledMessage.rewardId - ); - const rewardName = Object.keys(RedemptionIds)[rewardIdValueIndex]; - - handledMessage.rewardId = rewardName; - - broadcast(JSON.stringify(handledMessage)); - } - - // TODO: improve this check - const keepInQueueRewards = [RedemptionIds.KaraokeTime]; - - // @ts-expect-error String is not assignable to... but all keys are strings - if (keepInQueueRewards.includes(message.rewardId)) { - console.log(`${LOG_PREFIX}Reward kept in queue due to config`); - return; - } - - const completeOrCancelReward = - handledMessage && isProduction ? completeRewards : cancelRewards; - - if (message.rewardIsQueued) { - try { - await completeOrCancelReward( - message.channelId, - message.rewardId, - message.id - ); - console.log( - `${LOG_PREFIX}Reward removed from queue (completed or canceled)` - ); - } catch (e) { - if (e instanceof Error) { - console.log(`${LOG_PREFIX}${e.message}`); - } - } - } + console.log(`${LOG_PREFIX}Connected & registered`); } export { registerUserListener, LOG_PREFIX }; diff --git a/src/interfaces/actions/Action.ts b/src/interfaces/actions/Action.ts index 5b9670b..e2f1e0e 100644 --- a/src/interfaces/actions/Action.ts +++ b/src/interfaces/actions/Action.ts @@ -1,9 +1,10 @@ import { ActionType } from "../../enums/ActionType"; export interface Action { - type: ActionType; - channelId: string; - userId: string; - scheduledAt?: number; - data: any; + type: ActionType; + channelId: string; + userId: string; + scheduledAt?: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any; }