allow mods to be timed out, optional broadcasterAPI added

This commit is contained in:
2025-04-02 14:51:25 +02:00
parent a35d38c886
commit b3bd11e513
7 changed files with 109 additions and 21 deletions

View File

@@ -1,3 +1,5 @@
CLIENT_ID=
CLIENT_SECRET=
OAUTH_CODE=
DIFFERENT_BROADCASTER=false
BROADCASTER_OAUTH_CODE=

View File

@@ -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

View File

@@ -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": [],

View File

@@ -10,6 +10,8 @@ const bot = new Bot({
})
bot.onConnect(async ()=> {
console.log("Ready to accept commands!")
await authProvider.refreshAccessTokenForUser(238377856)
})
// await authProvider.refreshAccessTokenForUser(238377856)
setTimeout(() => {
console.log('Bot is ready to accept commands!')
}, 1000 * 3)
})

View File

@@ -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 }

View File

@@ -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 }

View File

@@ -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<statusmessage> {
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)
}