diff --git a/index.ts b/index.ts index 59bc537..62b1068 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,5 @@ import { PubSubClient, PubSubRedemptionMessage } from "twitch-pubsub-client"; -import { RefreshableAuthProvider, StaticAuthProvider } from "twitch-auth"; +import { getApiClient, getAuthProvider } from "./src/backend/helpers/twitch"; import { AddressInfo } from "net"; import { ApiClient } from "twitch"; @@ -9,7 +9,6 @@ import express from "express"; import { promises as fs } from "fs"; import path from "path"; -const TOKENS_FILE = "./tokens.json"; const SCHEDULED_FILE = "./scheduled.json"; const DEV_MODE = process.env.NODE_ENV === "development"; @@ -24,57 +23,9 @@ let chatClient: ChatClient; //! Important: store users & channels by id, not by username async function init() { - let tokenData; - try { - tokenData = JSON.parse((await fs.readFile(TOKENS_FILE)).toString()); - } catch (error) { - console.error(`${TOKENS_FILE} not found, cannot init chatbot.`); - process.exit(1); - } + const authProvider = await getAuthProvider(); - if ( - !tokenData.refresh_token || - !tokenData.access_token - ) { - console.error(`Missing parameters in ${TOKENS_FILE}, refresh_token or access_token.`); - process.exit(1); - } - - if ( - !process.env.TWITCH_CLIENT_ID || - !process.env.TWITCH_CLIENT_SECRET - ) { - console.error( - `Missing environment parameters TWITCH_CLIENT_ID or TWITCH_CLIENT_SECRET` - ); - process.exit(1); - } - - const authProvider = new RefreshableAuthProvider( - new StaticAuthProvider( - process.env.TWITCH_CLIENT_ID, - tokenData.access_token - ), - { - clientSecret: process.env.TWITCH_CLIENT_SECRET, - refreshToken: tokenData.refresh_token, - expiry: - tokenData.expiryTimestamp === null - ? null - : new Date(tokenData.expiryTimestamp), - onRefresh: async ({ accessToken, refreshToken, expiryDate }) => { - console.log("Tokens refreshed"); - const newTokenData = { - access_token: accessToken, - refresh_token: refreshToken, - expiryTimestamp: expiryDate === null ? null : expiryDate.getTime() - }; - await fs.writeFile(TOKENS_FILE, JSON.stringify(newTokenData)); - } - } - ); - - apiClient = new ApiClient({ authProvider }); + apiClient = await getApiClient(); const pubSubClient = new PubSubClient(); const userId = await pubSubClient.registerUserListener(apiClient, channel); @@ -84,26 +35,7 @@ async function init() { chatClient = new ChatClient(authProvider, { channels: [channel] }); - chatClient.onConnect(async () => { - 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); - } - }); + chatClient.onConnect(onConnect); chatClient.onDisconnect((e: any) => { console.log(`[ChatClient] Disconnected ${e.message}`); @@ -118,6 +50,27 @@ async function init() { 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}` diff --git a/src/backend/helpers/tokenData.ts b/src/backend/helpers/tokenData.ts new file mode 100644 index 0000000..2f6d006 --- /dev/null +++ b/src/backend/helpers/tokenData.ts @@ -0,0 +1,51 @@ +import { TokenData } from "../../interfaces/TokenData"; +import {promises as fs} from "fs"; +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: TokenData): Promise { + const tokenDataFilePath = getTokenDataFilePath(); + const jsonTokenData = JSON.stringify(tokenData); + + await fs.writeFile(tokenDataFilePath, jsonTokenData); + console.log(`${LOG_PREFIX}Token data saved`); +} + +function checkTokenData(tokenData: TokenData) { + if ( + !tokenData.access_token || + !tokenData.refresh_token + ) { + console.error(`${LOG_PREFIX}Missing refresh_token or access_token in ${TOKENS_FILE}.`); + process.exit(1); + } +} \ No newline at end of file diff --git a/src/backend/helpers/twitch.ts b/src/backend/helpers/twitch.ts new file mode 100644 index 0000000..8125d8c --- /dev/null +++ b/src/backend/helpers/twitch.ts @@ -0,0 +1,96 @@ +import { AccessToken, RefreshableAuthProvider, StaticAuthProvider } from "twitch-auth"; +import { getTokenData, saveTokenData } from "./tokenData"; + +import { ApiClient } from "twitch"; +import { TokenData } from "../../interfaces/TokenData"; + +const LOG_PREFIX = "[Twitch] "; + +let refreshAuthProvider: RefreshableAuthProvider; + +export { + getAuthProvider, + getApiClient +} + +interface ClientCredentials { + clientId: string; + clientSecret: string; +} + +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); + } + + return { + clientId: process.env.TWITCH_CLIENT_ID, + clientSecret: process.env.TWITCH_CLIENT_SECRET + }; +} + +async function createStaticAuthProvider(): Promise { + let tokenData = await getTokenData(); + const credentials = getClientCredentials(); + + return new StaticAuthProvider( + credentials.clientId, + tokenData.access_token + ); +} + +async function getAuthProvider(): Promise { + if (refreshAuthProvider) { + return refreshAuthProvider; + } + + let tokenData = await getTokenData(); + + const staticAuthProvider = await createStaticAuthProvider(); + const credentials = getClientCredentials(); + + const expiry = tokenData.expiryTimestamp === null + ? null + : new Date(tokenData.expiryTimestamp); + + refreshAuthProvider = new RefreshableAuthProvider( + staticAuthProvider, + { + clientSecret: credentials.clientSecret, + refreshToken: tokenData.refresh_token, + expiry, + onRefresh: onRefresh + } + ) as RefreshableAuthProvider; + + return refreshAuthProvider; +} + +async function onRefresh(refreshData: AccessToken): Promise { + const { accessToken, refreshToken, expiryDate } = refreshData; + console.log(`${LOG_PREFIX}Tokens refreshed`); + + const expiryTimestamp = expiryDate === null + ? 0 + : expiryDate.getTime() + + const newTokenData: TokenData = { + access_token: accessToken, + refresh_token: refreshToken, + expiryTimestamp + }; + + await saveTokenData(newTokenData); +} + +async function getApiClient() { + const authProvider = await getAuthProvider(); + + return new ApiClient({ authProvider }); +} diff --git a/src/interfaces/TokenData.ts b/src/interfaces/TokenData.ts new file mode 100644 index 0000000..0e05881 --- /dev/null +++ b/src/interfaces/TokenData.ts @@ -0,0 +1,5 @@ +export interface TokenData { + access_token: string; + refresh_token: string; + expiryTimestamp: number; +} \ No newline at end of file