From b3bd11e513ff0d761afb2592e4ccd13b9162f899 Mon Sep 17 00:00:00 2001 From: qwerinope Date: Wed, 2 Apr 2025 14:51:25 +0200 Subject: [PATCH] allow mods to be timed out, optional broadcasterAPI added --- .example.env | 2 ++ compose.yml | 9 +++-- pb/migrations/initialize.js | 9 +++++ src/bot.ts | 8 +++-- src/lib/api.ts | 4 ++- src/lib/auth.ts | 69 +++++++++++++++++++++++++++++++------ src/lib/timeoutHelper.ts | 29 +++++++++++++--- 7 files changed, 109 insertions(+), 21 deletions(-) diff --git a/.example.env b/.example.env index 22f6e7f..a464e54 100644 --- a/.example.env +++ b/.example.env @@ -1,3 +1,5 @@ CLIENT_ID= CLIENT_SECRET= OAUTH_CODE= +DIFFERENT_BROADCASTER=false +BROADCASTER_OAUTH_CODE= diff --git a/compose.yml b/compose.yml index 91cb65d..1eee310 100644 --- a/compose.yml +++ b/compose.yml @@ -12,8 +12,8 @@ services: environment: # These are only needed at first start. These are the values used to login to the admin panel. # If left empty the email will be set to test@example.com and the password to 1234567890 - #- EMAIL= - #- PASSWORD= + - EMAIL= + - PASSWORD= bot: depends_on: pocketbase: @@ -28,6 +28,11 @@ services: - action: rebuild path: ./src environment: + # These env variables can be removed once the bot has sucessfully run once - CLIENT_ID=$CLIENT_ID - CLIENT_SECRET=$CLIENT_SECRET - OAUTH_CODE=$OAUTH_CODE + # If the broadcaster is different from the bot user, + # the broadcaster will need to authorize the bot to perform certain actions + - DIFFERENT_BROADCASTER=$DIFFERENT_BROADCASTER + - BROADCASTER_OAUTH_CODE=$BROADCASTER_OAUTH_CODE diff --git a/pb/migrations/initialize.js b/pb/migrations/initialize.js index 5ce9b12..e4f0a9f 100644 --- a/pb/migrations/initialize.js +++ b/pb/migrations/initialize.js @@ -166,6 +166,15 @@ migrate(app => { "required": true, "system": false, "type": "json" + }, + { + "hidden": false, + "id": "bool3207122276", + "name": "main", + "presentable": false, + "required": false, + "system": false, + "type": "bool" } ], "indexes": [], diff --git a/src/bot.ts b/src/bot.ts index eabff25..8f8eb03 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -10,6 +10,8 @@ const bot = new Bot({ }) bot.onConnect(async ()=> { - console.log("Ready to accept commands!") - await authProvider.refreshAccessTokenForUser(238377856) -}) \ No newline at end of file + // await authProvider.refreshAccessTokenForUser(238377856) + setTimeout(() => { + console.log('Bot is ready to accept commands!') + }, 1000 * 3) +}) diff --git a/src/lib/api.ts b/src/lib/api.ts index 9443f6c..a0a5657 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,4 +1,6 @@ -import authProvider from "../lib/auth"; +import authProvider, { broadcasterAuthProvider as BCAuthFunction } from "../lib/auth"; import { ApiClient } from "@twurple/api"; const api = new ApiClient({ authProvider }) export default api +const broadcasterApi = BCAuthFunction !== undefined ? new ApiClient({ authProvider: await BCAuthFunction() }) : undefined +export { broadcasterApi } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index efac1a9..cbf4f89 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,25 +1,47 @@ import { RefreshingAuthProvider, exchangeCode } from '@twurple/auth' import pb from './pocketbase' +import { RecordModel } from 'pocketbase' -const ttvauth = await pb.collection('ttvauth').getFullList() +let ttvauth: RecordModel | undefined +try { + ttvauth = await pb.collection('ttvauth').getFirstListItem('main=true') +} catch (err) { + ttvauth = undefined +} -let auth = ttvauth.length === 0 ? await firstAccess() : ttvauth[0].auth +let auth = !ttvauth ? await firstAccess() : ttvauth.auth -async function firstAccess() { +let broadcasterAuthData: RecordModel | undefined +try { + broadcasterAuthData = await pb.collection('ttvauth').getFirstListItem('main=false') +} catch (err) { + broadcasterAuthData = undefined +} + +const DIFFERENT_BROADCASTER = process.env.DIFFERENT_BROADCASTER +broadcasterAuthData = !broadcasterAuthData && DIFFERENT_BROADCASTER === 'true' ? await firstAccess(false) : broadcasterAuthData ? broadcasterAuthData.auth : undefined + +async function firstAccess(main = true) { // This function gets the required auth codes, and stores it in pocketbase // The environment variables can be dropped after first run const CLIENT_ID = process.env.CLIENT_ID const CLIENT_SECRET = process.env.CLIENT_SECRET const OAUTH_CODE = process.env.OAUTH_CODE + const BROADCASTER_OAUTH_CODE = process.env.BROADCASTER_OAUTH_CODE if (!CLIENT_ID) { console.error("No 'CLIENT_ID' for OAuth defined in environment variables."); process.exit(1) } if (!CLIENT_SECRET) { console.error("No 'CLIENT_SECRET' for OAuth defined in environment variables."); process.exit(1) } - if (!OAUTH_CODE) { - console.error("No 'OAUTH_CODE' provided. To get the code, please visit this URL, authorize the bot and copy the 'code' from the return URL.") - console.error(`https://id.twitch.tv/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=http://localhost&response_type=code&scope=chat:read+chat:edit+moderator:manage:banned_users+moderation:read`) + if ((main && !OAUTH_CODE) || (!main && !BROADCASTER_OAUTH_CODE)) { + if (main) { + console.error("No 'OAUTH_CODE' provided. To get the code, please visit this URL, authorize the bot and copy the 'code' from the return URL.") + console.error(`https://id.twitch.tv/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=http://localhost&response_type=code&scope=chat:read+chat:edit+moderator:manage:banned_users+moderation:read`) + } else { + console.error("No 'BROADCASTER_OAUTH_CODE' provided. To get the code, please make the broadcaster visit the following URL, and get them to return the 'code' from the return URL.") + console.error(`https://id.twitch.tv/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=http://localhost&response_type=code&scope=moderator:manage:banned_users+moderation:read+channel:manage:moderators`) + } process.exit(1) } - const tokens = await exchangeCode(CLIENT_ID, CLIENT_SECRET, OAUTH_CODE, "http://localhost") + const tokens = await exchangeCode(CLIENT_ID, CLIENT_SECRET, main ? OAUTH_CODE! : BROADCASTER_OAUTH_CODE!, "http://localhost") const auth = { CLIENT_ID, CLIENT_SECRET, @@ -29,7 +51,7 @@ async function firstAccess() { OBTAINMENTTIMESTAMP: tokens.obtainmentTimestamp } - await pb.collection('ttvauth').create({ auth }) + await pb.collection('ttvauth').create({ auth, main }) return auth } @@ -54,10 +76,37 @@ authProvider.onRefresh(async (_id, newTokenData) => { auth.EXPIRESIN = newTokenData.expiresIn! auth.OBTAINMENTTIMESTAMP = newTokenData.obtainmentTimestamp - const ttvauthid = await pb.collection('ttvauth').getFullList() - await pb.collection('ttvauth').update(ttvauthid[0].id, { auth }) + const ttvauthid = await pb.collection('ttvauth').getFirstListItem('main=true') + await pb.collection('ttvauth').update(ttvauthid.id, { auth }) console.log("Refreshed OAuth tokens.") }) export default authProvider + +const broadcasterAuthProvider = broadcasterAuthData === undefined ? undefined : async () => { + const broadcasterAuthProvider = new RefreshingAuthProvider({ + clientId: broadcasterAuthData.CLIENT_ID, + clientSecret: broadcasterAuthData.CLIENT_SECRET + }) + await broadcasterAuthProvider.addUserForToken({ + accessToken: broadcasterAuthData.ACCESS_TOKEN, + refreshToken: broadcasterAuthData.REFRESH_TOKEN, + expiresIn: broadcasterAuthData.EXPIRESIN, + obtainmentTimestamp: broadcasterAuthData.OBTAINMENTTIMESTAMP + }, ['moderator:manage:banned_users', 'moderation:read', 'channel:manage:moderators']) + + broadcasterAuthProvider.onRefresh(async (_id, newTokenData) => { + broadcasterAuthData.ACCESS_TOKEN = newTokenData.accessToken + broadcasterAuthData.REFRESH_TOKEN = newTokenData.refreshToken! + broadcasterAuthData.EXPIRESIN = newTokenData.expiresIn! + broadcasterAuthData.OBTAINMENTTIMESTAMP = newTokenData.obtainmentTimestamp + + const broadcasterauthid = await pb.collection('ttvauth').getFirstListItem("main=false") + await pb.collection('ttvauth').update(broadcasterauthid.id, { auth: broadcasterAuthData }) + console.log("Refreshed Broadcaster OAuth tokens.") + }) + return broadcasterAuthProvider +} + +export { broadcasterAuthProvider } diff --git a/src/lib/timeoutHelper.ts b/src/lib/timeoutHelper.ts index cfec5dd..75c3a97 100644 --- a/src/lib/timeoutHelper.ts +++ b/src/lib/timeoutHelper.ts @@ -1,5 +1,5 @@ import { HelixUser } from "@twurple/api"; -import api from "./api"; +import api, { broadcasterApi } from "./api"; import pb from "./pocketbase"; import { getDBID } from "./userHelper"; @@ -7,16 +7,29 @@ type shooter = 'blaster' | 'grenade' | 'silverbullet' | 'watergun' | 'tnt' interface statusmessage { status: boolean, - reason?: string + reason: string } export async function timeout(broadcasterid: string, target: HelixUser, duration: number, reason: string): Promise { if (!target) return { status: false, reason: 'noexist' } - if (await api.moderation.checkUserBan(broadcasterid, target)) return { status: false, reason: 'banned' } + // if (target.name === 'qwerinope') return { status: false, reason: 'unknown' } + if (broadcasterApi) { + if (await broadcasterApi.moderation.checkUserBan(broadcasterid, target)) return { status: false, reason: 'banned' } + } else { + if (await api.moderation.checkUserBan(broadcasterid, target)) return { status: false, reason: 'banned' } + } try { - await api.moderation.banUser(broadcasterid, { duration, reason, user: target }) - return { status: true } + if (broadcasterApi) { + if (await broadcasterApi.moderation.checkUserMod(broadcasterid, target)) { + await broadcasterApi.moderation.removeModerator(broadcasterid, target) + remodMod(broadcasterid, target, duration) + } + await broadcasterApi.moderation.banUser(broadcasterid, { duration, reason, user: target }) + } else { + await api.moderation.banUser(broadcasterid, { duration, reason, user: target }) + } + return { status: true, reason: '' } } catch (err) { console.error(err) return { status: false, reason: 'unknown' } @@ -37,3 +50,9 @@ export async function addTimeoutToDB(attacker: HelixUser, target: HelixUser, sou } await pb.collection('timeouts').create(timeoutobj) } + +function remodMod(broadcasterid: string, target: HelixUser, duration: number) { + setTimeout(async () => { + await broadcasterApi?.moderation.addModerator(broadcasterid, target) + }, (duration + 3) * 1000) +}