Compare commits

...

27 Commits

Author SHA1 Message Date
d23d1a8f7d amendment to last commit 2025-05-10 14:09:27 +02:00
dae3d90397 Apply displayName instead of username whenever necessary 2025-05-10 01:23:34 +02:00
7843ebaa16 change references of qweri0p to qwerinope 2025-05-07 09:46:22 +02:00
88419e42a2 disable .env.* from being shared to github 2025-04-18 17:09:08 +02:00
b56f179717 stop item usage when another item is already in use 2025-04-18 16:47:50 +02:00
a1c93ef64b fix explosives when using aliases 2025-04-15 22:27:42 +02:00
adb0419aaf add leaderboard command 2025-04-14 17:03:10 +02:00
fc8cc82478 fix intent system 2025-04-13 20:56:37 +02:00
a2ab327c83 Merge #2: New items: revive and super revive 2025-04-13 20:30:48 +02:00
a5d3cad00b fix revive system, fix iteminfo, add revives to README 2025-04-13 17:58:30 +02:00
c2fac183cd add alpha-level revive and superrevive code 2025-04-13 00:34:42 +02:00
21c0c5db43 Add basic documentation for helper functions 2025-04-12 19:18:58 +02:00
d9418a3224 add automatic inventory version upgrades 2025-04-12 15:51:36 +02:00
576f28df9d add new plural system 2025-04-11 14:44:23 +02:00
e3fc3465e9 part 1 of full item/inventory rework 2025-04-11 12:48:20 +02:00
7b6f8bc07f fix !give negative numbers, !modme, automatic remod system (FUCK JAVASCRIPT) 2025-04-08 23:43:27 +02:00
9a2ce6b3a4 replace id row in users collection with twitchid
reduce spamming of pocketbase api
2025-04-07 20:34:02 +02:00
17f557f808 update development compose file and readme compose example 2025-04-07 19:46:11 +02:00
2268112920 move !modme and !getloot options to env variables 2025-04-07 13:24:08 +02:00
784650b0dc add !alltime command to readme 2025-04-06 23:00:24 +02:00
6215d3ee80 monthly stats implemented 2025-04-06 22:56:59 +02:00
9bba99dd53 fix tnt usage 2025-04-05 23:35:56 +02:00
2d43065a2a buncha fixes: unused items no longer get subtracted, bot can not get blasted, 3 decimal stats 2025-04-05 23:19:11 +02:00
3bd2cb1038 move itemuses to a seperate table, finish !stats implementation 2025-04-05 19:27:12 +02:00
5b5886e3a1 remove final use of any type in code 2025-04-05 18:37:18 +02:00
39b31e848c fix broken links in README.md 2025-04-05 18:35:10 +02:00
d565945428 fix clipboard functionality, add channel:read:polls intent 2025-04-05 18:09:54 +02:00
30 changed files with 709 additions and 407 deletions

View File

@@ -1,5 +1,7 @@
BOT_NAME=
CHANNEL=
MODS=
COOLDOWN=
CLIENT_ID=
CLIENT_SECRET=
REDIRECT_URI=https://qweri0p.github.io/url-params/

2
.gitignore vendored
View File

@@ -139,4 +139,4 @@ dist
pb/data
# config files
auth.json
.env.*

View File

@@ -1,6 +1,6 @@
# qweribot
A copy of twitch bot 'MandooBot' in [eddie's stream](twitch.tv/eddie).
A copy of twitch bot 'MandooBot' in [eddie's stream](https://twitch.tv/eddie).
## Usage
@@ -10,16 +10,17 @@ Here is the list of commands.
COMMAND|FUNCTION|USER|ALIASES
-|-|-|-
`!balance [target]`|List write the amount of money the user or the target user has.|anyone|`!bal, !qbucks, !qweribucks`
`!inventory [target]`|Show inventory contents of user or the target user.|anyone|`!inv`
`!getloot`|Give user a lootbox. This command has a cooldown that can be changed in `lootbox.ts`. You can optionally require the user to subscribe.|anyone|`None`
`!stats [target]`|Show the stats of user or target user including users shot, TNT used and grenades lobbed.|anyone|`None`
`!timeout {target}`|Give the target user a timeout of 60 seconds. This requires 100 qbucks.|anyone|`None`
`!balance [target]`|List write the amount of money the user or the target user has|anyone|`!bal, !qbucks, !qweribucks`
`!inventory [target]`|Show inventory contents of user or the target user|anyone|`!inv`
`!getloot`|Give user a lootbox. This command has a cooldown that can be changed with the `COOLDOWN` environment variable|anyone|`None`
`!stats [target]`|Show the stats of user or target user including users shot, TNT used and grenades lobbed of the current month|anyone|`None`
`!alltime [target]`|Show the stats of user or target user including users shot, TNT used and grenades lobbed of all time|anyone|`None`
`!timeout {target}`|Give the target user a timeout of 60 seconds. This requires 100 qbucks|anyone|`None`
`!use {item}`|Use a specific item. The user needs the specific item in their inventory. For items please look at the table below|anyone|`None`
`!iteminfo {item}`|Gives a description of the requested item. Identical to [the item descriptions in this document](#items)|anyone|`!item`
`!modme`|Gives the user moderator status. Only gives users moderator status if their name is in `modme.ts`|anyone|`None`
`!modme`|Gives the user moderator status. Only gives users moderator status if their name is in the `MODS` environment variable|anyone|`None`
`!give {target} {item} {count}`|Give a specific user a specific amount of an item. Negative amounts can be used to remove items|streamer|`None`
`!vulnchatters`|Print how many users are vulnerable to TNT and grenade explosions.|streamer|`None`
`!vulnchatters`|Print how many users are vulnerable to TNT and grenade explosions|streamer|`None`
### Items
@@ -30,6 +31,8 @@ ITEM|FUNCTION|ALIASES
-|-|-
`blaster {target}`|Times the target user out for 60 seconds|`!blast, !blaster`
`silverbullet {target}`|Times the target user out for 24 hours|`!execute, !silverbullet`
`revive {target}`|Reduce timeout timer of target by 30 seconds|`!revive, !heal`
`superrevive {target}`|Reduce timeout timer of target by 12|`!superrevive, !superheal`
`grenade`|Times a random chatter out for 60 seconds|`!grenade`
`tnt`|Times out 1 to 10 chatters for 60 seconds|`!tnt`
`lootbox`|Gives the user some qbucks, and possibly some items|`!lootbox`
@@ -46,19 +49,21 @@ The `compose.yaml` file in the repository is for development.
services:
qweribot:
container_name: qweribot
image: ghcr.io/qwerinope/qweribot:latest
image: ghcr.io/qwerinope/qweribot-bot:latest
environment: # The README.md has more detail on these config options
# Use the supplied .example.env for setting environment variables
- BOT_NAME=$BOT_NAME
- CHANNEL=$CHANNEL
- BOT_NAME=
- CHANNEL=
- MODS= # Separate the names of moderators with commas. Example: qwerinope,eponirewq,eddie
- COOLDOWN= # Optional
# The following environment variables can be removed after first setup
- CLIENT_ID=$CLIENT_ID
- CLIENT_SECRET=$CLIENT_SECRET
- REDIRECT_URI=$REDIRECT_URI
- OAUTH_CODE=$OAUTH_CODE # If this variable is left empty on starting, the bot will direct the user to a URL where the OAuth code can be obtained
- CLIENT_ID=
- CLIENT_SECRET=
- REDIRECT_URI=
- OAUTH_CODE= # If this variable is left empty on starting, the bot will direct the user to a URL where the OAuth code can be obtained
# The following environment variables need to only be set if the bot user and the streamer are not using the same account
- DIFFERENT_BROADCASTER=$DIFFERENT_BROADCASTER # Set to either true or false
- BROADCASTER_OAUTH_CODE=$BROADCASTER_OAUTH_CODE # As with OAUTH_CODE, leave empty for instructions
- DIFFERENT_BROADCASTER=false # Set to either true or false
- BROADCASTER_OAUTH_CODE= # As with OAUTH_CODE, leave empty for instructions
# Make sure that CLIENT_ID, CLIENT_SECRET and REDIRECT_URI are still set when enabling DIFFERENT_BROADCASTER after first setup
restart: no
depends_on:
@@ -67,7 +72,7 @@ services:
pocketbase:
container_name: pocketbase
image: ghcr.io/qwerinope/pocketbase-qweribot:latest
image: ghcr.io/qwerinope/qweribot-pocketbase:latest
# If environment variables are left empty, the default user & password will be: test@example.com and 1234567890
# This will only impact the login on http://localhost:8090
#environment:
@@ -83,7 +88,7 @@ services:
### Native (not recommended)
If you wish to run the bot not using docker, you will need to set up a [pocketbase](pocketbase.io) instance, and point the `PBURL` environment variable to the correct address.
If you wish to run the bot not using docker, you will need to set up a [pocketbase](https://pocketbase.io) instance, and point the `PBURL` environment variable to the correct address.
Also make sure the migration from `pb/migrations` is passed into the pocketbase instance.
Install dependencies with `bun install`.
@@ -97,10 +102,12 @@ Options with :bangbang: in the Required column need to be present for setup, and
VARIABLE|DEFAULT|FUNCTION|REQUIRED
-|-|-|-
`BOT_NAME`|None|Set the name of the bot user for Authentification|:white_check_mark:
`CHANNEL`|None| Set the name of the twitch channel to join|:white_check_mark:
`CHANNEL`|None|Set the name of the twitch channel to join|:white_check_mark:
`MODS`|None|List of users that can use `!modme` to give themselves moderator status|:white_check_mark:
`COOLDOWN`|24 Hours|Cooldown between letting users get a lootbox with `!getloot` in seconds|:x:
`CLIENT_ID`|None|Set the CLIENT_ID to authenticate the bot|:bangbang:
`CLIENT_SECRET`|None|Set the CLIENT_SECRET to authenticate the bot|:bangbang:
`REDIRECT_URI`|`https://qweri0p.github.io/url-params/`|The REDIRECT_URI set in the twitch dev console|:bangbang:
`REDIRECT_URI`|`https://qwerinope.github.io/url-params/`|The REDIRECT_URI set in the twitch dev console|:bangbang:
`OAUTH_CODE`|None|Authorization code for OAuth|:bangbang:
`DIFFERENT_BROADCASTER`|`false`|Set this to true when `BOT_NAME` and `CHANNEL` are different.|:white_check_mark:
`BROADCASER_OAUTH_CODE`|None|OAuth authorization code for the broadcaster (ignored if `DIFFERENT_BROADCASTER` is false)|:bangbang:

View File

@@ -30,6 +30,8 @@ services:
environment:
- BOT_NAME=$BOT_NAME
- CHANNEL=$CHANNEL
- MODS=$MODS
- COOLDOWN=$COOLDOWN
# The following env variables can be removed once the bot has sucessfully run once
- CLIENT_ID=$CLIENT_ID
- CLIENT_SECRET=$CLIENT_SECRET

View File

@@ -14,6 +14,81 @@ migrate(app => {
app.save(record)
const data = [
{
"id": "pbc_279239694",
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": null,
"deleteRule": null,
"name": "itemuses",
"type": "base",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 0,
"min": 0,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"cascadeDelete": false,
"collectionId": "pbc_3754236674",
"hidden": false,
"id": "relation1542800728",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"indexes": [],
"system": false
},
{
"id": "pbc_1170220047",
"listRule": "",
@@ -28,8 +103,8 @@ migrate(app => {
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"max": 0,
"min": 0,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
@@ -193,8 +268,8 @@ migrate(app => {
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"max": 0,
"min": 0,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
@@ -203,20 +278,6 @@ migrate(app => {
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3605594593",
"max": 0,
"min": 0,
"name": "twitchid",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
@@ -227,7 +288,7 @@ migrate(app => {
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": true,
"required": false,
"system": false,
"type": "text"
},
@@ -241,16 +302,6 @@ migrate(app => {
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json1357546519",
"maxSize": 0,
"name": "itemuses",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "number2901680126",
@@ -295,9 +346,7 @@ migrate(app => {
"type": "autodate"
}
],
"indexes": [
"CREATE UNIQUE INDEX `idx_MR0pV8SUAF` ON `users` (`twitchid`)"
],
"indexes": [],
"system": false
}
]

View File

@@ -6,8 +6,10 @@ import api, { broadcasterAuthProvider } from './lib/api';
import { removeVulnChatter, vulnerableUsers } from './lib/timeoutHelper';
const channel = process.env.CHANNEL ?? ''
const user = process.env.BOT_NAME ?? ''
const user = process.env.BOT_NAME
if (!user) { console.error("Please set the BOT_NAME environment variable."); process.exit(1) }
const channel = process.env.CHANNEL
if (!channel) { console.error("Please set the CHANNEL environment variable."); process.exit(1) }
const bot = new Bot({
authProvider,
@@ -23,7 +25,7 @@ bot.onConnect(async () => {
await broadcasterAuthProvider.refreshAccessTokenForUser(broadcastername?.id!)
}
setTimeout(() => {
setTimeout(async () => {
console.log('Bot is ready to accept commands!')
}, 1000 * 5)
})

View File

@@ -1,10 +1,11 @@
import { createBotCommand } from "@twurple/easy-bot";
import api from "../lib/api";
import items from "../items"
import { changeItemCount } from "../lib/items";
import { changeBalance } from "../lib/userHelper";
import { vulnerableUsers } from "../lib/timeoutHelper";
const give = createBotCommand('give', async (params, { say, broadcasterId, userId, userName }) => {
const give = createBotCommand('give', async (params, { say, broadcasterId, userId }) => {
if (userId !== broadcasterId) return
const target = await api.users.getUserByName(params[0].replace(/[@]/g, ''))
@@ -12,15 +13,16 @@ const give = createBotCommand('give', async (params, { say, broadcasterId, userI
if (isNaN(parseInt(params[2]))) { await say(`Specify the amount`); return }
const data = params[1].toLowerCase() === 'qbucks' ? await changeBalance(target, parseInt(params[2])) : await changeItemCount(target, params[1].toLowerCase(), parseInt(params[2]))
const data = params[1].toLowerCase() === 'qbucks' ? await changeBalance(target, parseInt(params[2])) : await changeItemCount(target, params[1].toLowerCase(), parseInt(params[2]), true)
if (data.reason === 'negative') { await say(`${target.name} only has ${data.count}. Cannot yoink ${-parseInt(params[2])} ${params[1]}`); return }
else if (data.reason === 'noexist') { await say(`Can't find item ${params[1]}`); return }
await say(`${target.name} now has ${data.count} ${params[1]}`)
const selection = items.find(item => item.name === params[1].toLowerCase())
await say(`${target.displayName} now has ${data.count} ${params[1]}${data.count === 1 ? '' : selection?.plural}`)
})
const vulnChatters = createBotCommand('vulnchatters', async (_params, { say, userId, broadcasterId, userName }) => {
const vulnChatters = createBotCommand('vulnchatters', async (_params, { say, userId, broadcasterId }) => {
if (userId !== broadcasterId) return
await say(`There are ${vulnerableUsers.length} vulnerable chatters`)

View File

@@ -13,11 +13,9 @@ function getTimeDifference(date1: number, date2: number) {
return { days, hours, minutes, seconds };
}
export default createBotCommand('getloot', async (_params, { reply, userId, broadcasterId }) => {
export default createBotCommand('getloot', async (_params, { reply, userId }) => {
const user = await api.users.getUserById(userId)
// Remove the comment on the following line to only give lootboxes to subscribed users
//if (!user?.isSubscribedTo(broadcasterId)) { await reply('Subscribe to get loot :)'); return }
const data = await lootboxReady(user)
const data = await lootboxReady(user!)
if (!data.result) {
const { days, hours, minutes, seconds } = getTimeDifference(data.lastlootbox, Date.now() - COOLDOWN)
await reply(`lootbox ready in:

View File

@@ -1,13 +1,14 @@
import timeout from "./timeout";
import inventory from "./inventory";
import stats from "./stats";
import qbucks from "./qbucks";
import getloot from "./getloot";
import modme from "./modme";
import use from "./use";
import iteminfo from "./iteminfo"
import leaderboard from "./leaderboard"
import aliases from './itemAliases'
import admin from './admin'
import stats from "./stats"
import aliases from "./itemAliases"
import admin from "./admin"
export default [timeout, inventory, stats, qbucks, getloot, modme, use, iteminfo, ...aliases, ...admin]
export default [timeout, inventory, qbucks, getloot, modme, use, iteminfo, leaderboard, ...aliases, ...admin, ...stats]

View File

@@ -1,12 +1,13 @@
import { createBotCommand } from "@twurple/easy-bot";
import { getInventory } from "../lib/userHelper";
import api from "../lib/api";
import items from "../items";
import { HelixUser } from "@twurple/api";
export default createBotCommand('inv', async (params, { userName, say }) => {
export default createBotCommand('inv', async (params, { userName, say, userDisplayName }) => {
let user: HelixUser | null
if (params.length !== 0) {
user = await api.users.getUserByName(params[0].replace(/[^a-zA-Z0-9]/g, ''))
user = await api.users.getUserByName(params[0].replace(/[@]/g, ''))
} else user = await api.users.getUserByName(userName)
if (!user) {
await say(`User ${params[0]} not found`)
@@ -15,38 +16,18 @@ export default createBotCommand('inv', async (params, { userName, say }) => {
const data = params.length === 0 ? { me: true, inv: await getInventory(user!) } : { me: false, inv: await getInventory(user!) }
interface parsedData {
amount: number,
name: string,
plural: string
const messagedata: string[] = []
for (const [key, amount] of Object.entries(data.inv)) {
if (amount === 0) continue
const itemselection = items.find(item => item.name === key)
messagedata.push(`${itemselection?.prettyname}${amount === 1 ? '' : itemselection?.plural}: ${amount}`)
}
let dataparsed: parsedData[] = []
for (const key of Object.entries(data.inv)) {
if (key[1] === 0) continue
switch (key[0]) {
case 'lootbox':
dataparsed.push({ amount: key[1], name: key[0], plural: 'es' })
break
case 'version':
break
default:
dataparsed.push({ amount: key[1], name: key[0], plural: 's' })
break
}
}
if (!dataparsed) { await say(`${data.me ? userName : params[0]} has no items!`); return }
let messagedata: string[] = []
for (const item of dataparsed) {
messagedata.push(`${item.name + (item.amount === 1 ? '' : item.plural)}: ${item.amount}`)
}
if (messagedata.length === 0) {await say(`${data.me ? userName : params[0]} has no items mandoooYikes`); return}
if (messagedata.length === 0) { await say(`${data.me ? userDisplayName : params[0]} has no items!`); return }
await say(`
inventory of ${data.me ? userName : params[0]}:
inventory of ${data.me ? userDisplayName : user.displayName}:
${messagedata.join(', ')}
`)
}, { aliases: ['inventory'] })

View File

@@ -1,39 +1,38 @@
import { createBotCommand } from "@twurple/easy-bot";
import { BotCommand, createBotCommand } from "@twurple/easy-bot";
import { useBlaster, useClipboard, useGrenade, useLootbox, useSilverBullet, useTNT } from "../lib/items";
import api from "../lib/api";
import items from "../items";
import { ITEMBUSY, toggleBusy } from "../lib/items";
const blaster = createBotCommand('blaster', async (params, { say, broadcasterId, userId }) => {
const aliascommands: BotCommand[] = []
for (const item of items) {
aliascommands.push(createBotCommand(item.name, async (params, { say, reply, broadcasterId, userId }) => {
if (ITEMBUSY) { await reply(`There is currently an item in use. Try again.`); return }
const user = await api.users.getUserById(userId)
if (params[0] === undefined) return
await useBlaster(broadcasterId, user!, params[0].replace(/[@]/g, ''), say)
}, { aliases: ['blast'] })
toggleBusy()
switch (item.name) {
case 'blaster':
case 'silverbullet':
case 'revive':
case 'superrevive':
if (params[0] === undefined) { await reply('Please specify a target'); return }
await item.execute(user!, say, broadcasterId, params[0].replace(/[@]/g, ''))
break
case 'grenade':
case 'tnt':
await item.execute(user!, say, broadcasterId)
break
case 'lootbox':
await item.execute(user!, say)
break
case 'clipboard':
if (params[0] === undefined) { await reply("Please specify what the clipboard asks") }
await item.execute(user!, say, broadcasterId, params.join(' '))
break
}
toggleBusy()
}, { aliases: item.aliases }))
}
const silverbullet = createBotCommand('execute', async (params, { say, broadcasterId, userId }) => {
const user = await api.users.getUserById(userId)
if (params[0] === undefined) return
await useSilverBullet(broadcasterId, user!, params[0].replace(/[@]/g, ''), say)
}, { aliases: ['silverbullet'] })
const grenade = createBotCommand('grenade', async (_params, { say, broadcasterId, userId }) => {
const user = await api.users.getUserById(userId)
await useGrenade(broadcasterId, user!, say)
})
const tnt = createBotCommand('tnt', async (_params, { say, broadcasterId, userId }) => {
const user = await api.users.getUserById(userId)
await useTNT(broadcasterId, user!, say)
})
const lootbox = createBotCommand('lootbox', async (_params, { say, userId }) => {
const user = await api.users.getUserById(userId)
await useLootbox(user!, say)
})
const clipboard = createBotCommand('clipboard', async (params, { say, broadcasterId, userId }) => {
const user = await api.users.getUserById(userId)
if (params[0] === undefined) return
await useClipboard(broadcasterId, user!, params.join(' '), say)
})
export default [blaster, silverbullet, grenade, tnt, lootbox, clipboard]
export default aliascommands

View File

@@ -1,31 +1,9 @@
import { createBotCommand } from "@twurple/easy-bot";
import items from "../items"
export default createBotCommand('iteminfo', async (params, { say }) => {
if (params[0] === undefined) { await say('No item specified!'); return }
let message = ''
switch (params[0].toLowerCase()) {
case 'blaster':
message = "Item: blaster {target}, Function: Times the target user out for 60 seconds. Aliases: !blast, !blaster"
break
case 'silver':
case 'silverbullet':
message = "`Item: silverbullet {target}, Function: Times the target user out for 24 hours. Aliases: !execute, !silverbullet"
break
case 'grenade':
message = "Item: grenade, Function: Times a random chatter out for 60 seconds. Aliases: !grenade"
break
case 'tnt':
message = "Item: tnt, Function: Times out 1 to 10 chatters for 60 seconds. Aliases: !tnt"
break
case 'lootbox':
message = "Item: lootbox, Function: Gives the user some qbucks, and possibly some items. Aliases: !lootbox"
break
case 'clipboard':
message = "Item: clipboard {message}, Function: Starts a two minute long poll with the user specified message. Aliases: !clipboard"
break
default:
message = "Item not found"
break
}
await say(message)
const selection = items.find(item => item.aliases.includes(params[0].toLowerCase()))
if (!selection) { await say('Item not found'); return }
await say(selection.description)
}, { aliases: ['item'] })

View File

@@ -0,0 +1,22 @@
import { createBotCommand } from "@twurple/easy-bot"
import pb, { User } from "../lib/pocketbase"
import { getTimeouts } from "../lib/userHelper"
type KDData = { user: User, KD: number }
export default createBotCommand('leaderboard', async (_params, { say }) => {
const users = await pb.collection('users').getFullList()
let userKDs: KDData[] = []
for (const user of users) {
const data = await getTimeouts(user.id)
if (data.hit.blaster < 5) continue
const KD = data.shot.blaster / data.hit.blaster
const obj: KDData = { user, KD }
userKDs.push(obj)
}
if (userKDs.length === 0) { await say('No users on leaderboard yet!'); return }
userKDs.sort((data1, data2) => data2.KD - data1.KD)
const textlist: string[] = []
for (let i = 0; i < (userKDs.length < 5 ? userKDs.length : 5); i++) textlist.push(`${i + 1}. ${userKDs.at(i)!.user.firstname}: ${userKDs.at(i)!.KD.toFixed(2)}`)
await say(`${textlist.join(' | ')}`)
}, { aliases: ['kdleaderboard'] })

View File

@@ -1,11 +1,10 @@
import { createBotCommand } from "@twurple/easy-bot";
import api, { broadcasterApi } from "../lib/api";
import { MODS } from "../lib/timeoutHelper";
const MODS = ['qwerinope']
export default createBotCommand('modme', async (_params, { userName, broadcasterId, userId }) => {
if (!MODS!.includes(userName)) return
export default createBotCommand('modme', async (_params, { userName, broadcasterId }) => {
if (!MODS.includes(userName)) return
if (broadcasterApi) await broadcasterApi.moderation.addModerator(broadcasterId, userName)
else await api.moderation.addModerator(broadcasterId, userName)
if (broadcasterApi) await broadcasterApi.moderation.addModerator(broadcasterId, userId)
else await api.moderation.addModerator(broadcasterId, userId)
})

View File

@@ -14,6 +14,6 @@ export default createBotCommand('balance', async (params, { userName, say }) =>
}
const data = await getBalance(user)
await say(`${user.name} has ${data.balance} qbucks ${data.balance === 0 ? 'mandoooYikes' : 'mandoooSmile'}`)
await say(`${user.displayName} has ${data.balance} qbucks`)
}, { aliases: ['qbucks', 'qweribucks', 'bal'] })

View File

@@ -3,7 +3,36 @@ import api from "../lib/api";
import { getStats } from "../lib/userHelper";
import { HelixUser } from "@twurple/api";
export default createBotCommand('stats', async (params, { say, userName }) => {
const stats = createBotCommand('stats', async (params, { say, userName, userDisplayName }) => {
let user: HelixUser | null
if (params.length !== 0) {
user = await api.users.getUserByName(params[0].replace(/[@]/g, ''))
} else user = await api.users.getUserByName(userName)
if (!user) {
await say(`User ${params[0]} not found`)
return
}
const monthdata = new Date().toISOString().slice(0, 7)
const data = params.length === 0 ? { me: true, stats: await getStats(user!, monthdata) } : { me: false, stats: await getStats(user!, monthdata) }
const KD = data.stats.shot.blaster / data.stats.hit.blaster
await say(
`
THIS MONTH: Stats of ${data.me ? userDisplayName : user.displayName}:
Users blasted: ${data.stats.shot.blaster},
Blasted by others: ${data.stats.hit.blaster} (${isNaN(KD) ? 0 : KD.toFixed(2)} K/D).
Grenades lobbed: ${data.stats.used.grenade}
TNTs lit: ${data.stats.used.tnt},
Silver bullets fired: ${data.stats.shot.silverbullet},
Silver bullets taken: ${data.stats.hit.silverbullet}
`
)
})
const alltime = createBotCommand('alltime', async (params, { say, userName, userDisplayName }) => {
let user: HelixUser | null
if (params.length !== 0) {
user = await api.users.getUserByName(params[0].replace(/[@]/g, ''))
@@ -19,9 +48,9 @@ export default createBotCommand('stats', async (params, { say, userName }) => {
await say(
`
Stats of ${data.me ? userName : params[0]}:
ALLTIME: Stats of ${data.me ? userDisplayName : user.displayName}:
Users blasted: ${data.stats.shot.blaster},
Blasted by others: ${data.stats.hit.blaster} (${isNaN(KD) ? 0 : KD} K/D).
Blasted by others: ${data.stats.hit.blaster} (${isNaN(KD) ? 0 : KD.toFixed(2)} K/D).
Grenades lobbed: ${data.stats.used.grenade}
TNTs lit: ${data.stats.used.tnt},
Silver bullets fired: ${data.stats.shot.silverbullet},
@@ -29,3 +58,5 @@ export default createBotCommand('stats', async (params, { say, userName }) => {
`
)
})
export default [stats, alltime]

View File

@@ -3,7 +3,7 @@ import { addTimeoutToDB, timeout } from "../lib/timeoutHelper";
import { changeBalance, getBalance } from "../lib/userHelper";
import api from "../lib/api";
export default createBotCommand('timeout', async (params, { say, broadcasterId, userName }) => {
export default createBotCommand('timeout', async (params, { say, broadcasterId, userName, userDisplayName }) => {
const attacker = await api.users.getUserByName(userName)
const userbal = await getBalance(attacker!)
if (userbal.balance < 100) { await say('not enough qbucks'); return }
@@ -11,7 +11,7 @@ export default createBotCommand('timeout', async (params, { say, broadcasterId,
const target = await api.users.getUserByName(params[0].replace(/[@]/g, ''))
const status = await timeout(broadcasterId, target!, 60, `You got blasted by ${userName}`)
if (status.status) {
await say(`${params[0]} got blasted by ${userName}! mandoooGOTTEM ${userName} now has ${userbal.balance - 100} qbucks remaining`)
await say(`${target?.displayName} got blasted by ${userDisplayName}! ${userDisplayName} now has ${userbal.balance - 100} qbucks remaining`)
await changeBalance(attacker!, -100)
await addTimeoutToDB(attacker!, target!, 'blaster')
}
@@ -21,7 +21,7 @@ export default createBotCommand('timeout', async (params, { say, broadcasterId,
await say(`${params[0]} doesn't exist!`)
break
case 'banned':
await say(`${params[0]} is already dead!`)
await say(`${target?.displayName} is already dead!`)
break
case 'unknown':
await say(`NO!`)

View File

@@ -1,37 +1,38 @@
import { createBotCommand } from "@twurple/easy-bot";
import { useBlaster, useClipboard, useGrenade, useLootbox, useSilverBullet, useTNT } from "../lib/items";
import api from "../lib/api";
import items from "../items";
import { ITEMBUSY, toggleBusy } from "../lib/items";
export default createBotCommand('use', async (params, { say, broadcasterId, userId }) => {
export default createBotCommand('use', async (params, { say, reply, broadcasterId, userId }) => {
const user = await api.users.getUserById(userId)
if (params[0] === undefined) return
const selection = items.find(item => item.aliases.includes(params[0].toLowerCase()))
switch (params[0].toLowerCase()) {
if (!selection) { reply(`${params[0]} does not exist!`); return }
if (ITEMBUSY) { await reply(`There is currently an item in use. Try again.`); return }
toggleBusy()
switch (selection.name) {
case 'blaster':
if (params[1] === undefined) return
await useBlaster(broadcasterId, user!, params[1].replace(/[@]/g, ''), say)
break
case 'silver':
case 'silverbullet':
if (params[1] === undefined) return
await useSilverBullet(broadcasterId, user!, params[1].replace(/[@]/g, ''), say)
case 'revive':
case 'superrevive':
if (params[1] === undefined) { await reply('Please specify a target'); return }
await selection.execute(user!, say, broadcasterId, params[1].replace(/[@]/g, ''))
break
case 'grenade':
await useGrenade(broadcasterId, user!, say)
break
case 'tnt':
await useTNT(broadcasterId, user!, say)
await selection.execute(user!, say, broadcasterId)
break
case 'lootbox':
case 'loot':
await useLootbox(user!, say)
await selection.execute(user!, say)
break
case 'clipboard':
if (params[1] === undefined) return
await useClipboard(broadcasterId, user!, params.slice(1).join(' '), say)
if (params[1] === undefined) { await reply("Please specify what the clipboard asks") }
await selection.execute(user!, say, broadcasterId, params.slice(1).join(' '))
break
default:
await say(`${params[0]} does not exist mandoooYikes`)
}
toggleBusy()
})

76
src/items/blasters.ts Normal file
View File

@@ -0,0 +1,76 @@
import { HelixUser } from "@twurple/api";
import api from "../lib/api";
import { timeout, addTimeoutToDB } from "../lib/timeoutHelper";
import { addUsedItem, updateInventory } from "../lib/userHelper";
import { changeItemCount } from "../lib/items";
export const blaster = {
name: 'blaster',
prettyname: 'Blaster',
aliases: ['blast', 'blaster'],
plural: 's',
description: "Use: blaster {target}, Function: Times the target user out for 60 seconds. Aliases: !blast, !blaster",
execute: async (user: HelixUser, say: (arg0: string) => Promise<void>, broadcasterId: string, targetname: string) => {
const target = await api.users.getUserByName(targetname)
const itemResult = await changeItemCount(user, 'blaster')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no blasters!'); return }
const result = await timeout(broadcasterId, target, 60, `You got blasted by ${user.displayName}`)
if (result.status) {
await say(`${target?.displayName} got blasted by ${user.displayName}! ${user.displayName} has ${itemResult.count} blaster${itemResult.count === 1 ? '' : 's'} remaining`)
await addTimeoutToDB(user, target!, 'blaster')
await addUsedItem(user, 'blaster')
await updateInventory(user, itemResult.inv!)
} else {
switch (result.reason) {
case 'noexist':
await say(`${targetname} doesn't exist!`)
break
case 'banned':
await say(`${target?.displayName} is already dead!`)
break
case 'unknown':
await say(`NO!`)
await timeout(broadcasterId, user, 60, "NO!")
break
}
}
}
}
export const silverbullet = {
name: 'silverbullet',
prettyname: 'Silver Bullet',
plural: 's',
aliases: ['execute', 'silver', 'silverbullet'],
description: "Use: silverbullet {target}, Function: Times the target user out for 24 hours. Aliases: !execute, !silverbullet",
execute: async (user: HelixUser, say: (arg0: string) => Promise<void>, broadcasterId: string, targetname: string) => {
const target = await api.users.getUserByName(targetname)
const itemResult = await changeItemCount(user, 'silverbullet')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no silver bullets!'); return }
const result = await timeout(broadcasterId, target, 60 * 60 * 24, `You got hit by a silver bullet fired by ${user.displayName}`)
if (result.status) {
await say(`${target?.name} got deleted.`)
await addTimeoutToDB(user, target!, 'silverbullet')
await addUsedItem(user, 'silverbullet')
await updateInventory(user, itemResult.inv!)
} else {
switch (result.reason) {
case 'noexist':
await say(`${targetname} doesn't exist!`)
break
case 'banned':
await say(`${target?.displayName} is already dead!`)
break
case 'unknown':
await say(`NO!`)
await timeout(broadcasterId, user, 60, "NO!")
break
}
}
}
}

28
src/items/clipboard.ts Normal file
View File

@@ -0,0 +1,28 @@
import { HelixUser } from "@twurple/api";
import api, { broadcasterApi } from "../lib/api"
import { changeItemCount } from "../lib/items";
import { addUsedItem, updateInventory } from "../lib/userHelper";
export const clipboard = {
name: 'clipboard',
prettyname: 'Clipboard',
aliases: ['clipboard'],
plural: 's',
description: "Use: clipboard {message}, Function: Starts a two minute long poll with the user specified message. Aliases: !clipboard",
execute: async (user: HelixUser, say: (arg0: string) => Promise<void>, broadcasterId: string, question: string) => {
const tempapi = broadcasterApi ?? api
const polldata = await tempapi.polls.getPolls(broadcasterId)
const activepolldata = polldata.data.filter(poll => poll.status === "ACTIVE")
if (activepolldata.length > 0) { await say('Can\'t have two polls active at once.'); return }
const itemResult = await changeItemCount(user, 'clipboard')
await addUsedItem(user, 'clipboard')
await updateInventory(user, itemResult.inv!)
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no clipboards!'); return }
await tempapi.polls.createPoll(broadcasterId, { choices: ['Yes', 'No'], duration: 120, title: question })
await say(`${user.displayName} used a clipboard! They have ${itemResult.count} clipboard${itemResult.count === 1 ? '' : 's'} remaining`)
}
}

76
src/items/explosives.ts Normal file
View File

@@ -0,0 +1,76 @@
import api from "../lib/api";
import { addTimeoutToDB } from "../lib/timeoutHelper";
import { addUsedItem, updateInventory } from "../lib/userHelper";
import { changeItemCount } from "../lib/items";
import { vulnerableUsers, timeout } from "../lib/timeoutHelper";
import { HelixUser } from "@twurple/api";
function shuffle(arrayold: any[]) {
let array = arrayold
let currentIndex = array.length;
while (currentIndex != 0) {
let randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
}
return array
}
export const grenade = {
name: 'grenade',
prettyname: 'Grenade',
aliases: ['grenade'],
plural: 's',
description: "Use: grenade, Function: Times a random chatter out for 60 seconds. Aliases: !grenade",
execute: async (user: HelixUser, say: (arg0: string) => Promise<void>, broadcasterId: string) => {
if (vulnerableUsers.length === 0) { await say('No chatters to blow up!'); return }
const itemResult = await changeItemCount(user, 'grenade')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no grenades!'); return }
const target = await api.users.getUserById(vulnerableUsers[Math.floor(Math.random() * vulnerableUsers.length)])
const result = await timeout(broadcasterId, target!, 60, `You got hit by ${user.displayName}'s grenade`)
if (result.status) {
await say(`${target?.displayName} got blown up by ${user.displayName}'s grenade!`)
await addTimeoutToDB(user, target!, 'grenade')
await addUsedItem(user, 'grenade')
await updateInventory(user, itemResult.inv!)
} else {
// Banned is not an option, and neither is noexist
await say(`something went wrong`)
console.error(result.reason)
}
}
}
export const tnt = {
name: 'tnt',
prettyname: 'TNT',
aliases: ['tnt'],
plural: 's',
description: "Use: tnt, Function: Times out 1 to 10 chatters for 60 seconds. Aliases: !tnt",
execute: async (user: HelixUser, say: (args0: string) => Promise<void>, broadcasterId: string) => {
if (vulnerableUsers.length === 0) { await say('No chatters to blow up!'); return }
const itemResult = await changeItemCount(user, 'tnt')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no TNT!'); return }
const min = vulnerableUsers.length < 3 ? vulnerableUsers.length : 3 //if less than 3 chatters, use that else 3
const max = vulnerableUsers.length > 10 ? 10 : vulnerableUsers.length //if more than 10 chatters do 10 else 10
const blastedusers = Math.floor(Math.random() * (max - min + 1)) + min
const soontobedeadusers = shuffle(vulnerableUsers).slice(vulnerableUsers.length - blastedusers)
const targets = await api.users.getUsersByIds(soontobedeadusers)
for (const target of targets) {
const result = await timeout(broadcasterId, target!, 60, `You got hit by ${user.displayName}'s TNT`)
if (result.status) {
await say(`${target?.displayName} got blown up by TNT!`)
await addTimeoutToDB(user, target!, 'tnt')
await updateInventory(user, itemResult.inv!)
} else {
await say(`something went wrong`)
console.error(result.reason)
}
}
await addUsedItem(user, 'tnt')
await say(`${user.displayName} blew up ${blastedusers} chatters with their TNT! ${user.displayName} has ${itemResult.count} tnt${itemResult.count === 1 ? '' : 's'} remaining`)
}
}

19
src/items/index.ts Normal file
View File

@@ -0,0 +1,19 @@
import { HelixUser } from "@twurple/api"
import { blaster, silverbullet } from "./blasters"
import { grenade, tnt } from "./explosives"
import { revive, superrevive } from "./revives"
import { lootbox } from "./lootbox"
import { clipboard } from "./clipboard"
interface item {
name: string,
prettyname: string,
aliases: string[],
plural: string,
description: string,
execute: (user: HelixUser, say: (args0: string) => Promise<void>, broadcasterId?: string, targetname?: string) => Promise<void>
}
const data = [blaster, silverbullet, grenade, tnt, revive, superrevive, lootbox, clipboard] as item[]
export const ITEMS = data.map(item => item.name)
export default data

32
src/items/lootbox.ts Normal file
View File

@@ -0,0 +1,32 @@
import { HelixUser } from "@twurple/api";
import { changeItemCount } from "../lib/items";
import { addUsedItem, changeBalance, getInventory, updateInventory } from "../lib/userHelper";
function getRandom(): number {
return Math.floor(Math.random() * 100)
}
export const lootbox = {
name: 'lootbox',
prettyname: 'Lootbox',
aliases: ['lootbox', 'loot'],
plural: 'es',
description: "Use: lootbox, Function: Gives the user some qbucks, and possibly some items. Aliases: !lootbox",
execute: async (user: HelixUser, say: (arg0: string) => Promise<void>) => {
const itemResult = await changeItemCount(user, 'lootbox')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no lootboxes!'); return }
// Lootbox logic will for now just be get 25 qbucks, with 50% chance to get a grenade 25% chance to get a blaster and 10% chance to get TNT
let inventory = await getInventory(user)
let newitems: string[] = []
await changeBalance(user, 25)
newitems.push('25 qbucks')
if (getRandom() <= 50) { newitems.push('1 grenade'); inventory.grenade += 1 }
if (getRandom() <= 25) { newitems.push('1 blaster'); inventory.blaster += 1 }
if (getRandom() <= 10) { newitems.push('1 tnt'); inventory.tnt += 1 }
inventory.lootbox = itemResult.inv!.lootbox
await updateInventory(user, inventory)
await addUsedItem(user, 'lootbox')
await say(`${user.displayName} got: ${newitems.join(' and ')}`)
}
}

73
src/items/revives.ts Normal file
View File

@@ -0,0 +1,73 @@
import { HelixUser } from "@twurple/api"
import api from "../lib/api"
import { changeItemCount } from "../lib/items"
import { reviveTarget } from "../lib/timeoutHelper"
import { addUsedItem, updateInventory } from "../lib/userHelper"
export const revive = {
name: 'revive',
prettyname: 'Revive',
aliases: ['revive', 'heal'],
plural: 's',
description: "Use: revive {target}, Function: Reduce timeout timer of target by 30 seconds. Aliases: !revive, !heal",
execute: async (user: HelixUser, say: (arg0: string) => Promise<void>, broadcasterId: string, targetname: string) => {
const target = await api.users.getUserByName(targetname)
const itemResult = await changeItemCount(user, 'revive')
if (!itemResult.result) { await say('You have no revives!'); return }
const reviveResult = await reviveTarget(broadcasterId, target, 30)
if (reviveResult.status) { await updateInventory(user, itemResult.inv!); await addUsedItem(target!, 'revive') }
switch (reviveResult.reason) {
case 'noexist':
await say(`${targetname} does not exist`)
break
case 'notbanned':
await say(`${target?.displayName} doesn't need revives`)
break
case 'unknown':
await say("Something went wrong!")
break
case 'healed':
await say(`${target?.displayName} got healed for 30 seconds by ${user.displayName}`)
break
case 'revived':
await say(`${target?.displayName} got revived by ${user.displayName}`)
break
}
}
}
export const superrevive = {
name: 'superrevive',
prettyname: 'Super Revive',
aliases: ['superrevive', 'superheal'],
plural: 's',
description: "Use: superrevive {target}, Function: Reduce timeout timer of target by 12 hours. Aliases: !superrevive, !superheal",
execute: async (user: HelixUser, say: (arg0: string) => Promise<void>, broadcasterId: string, targetname: string) => {
const target = await api.users.getUserByName(targetname)
const itemResult = await changeItemCount(user, 'superrevive')
if (!itemResult.result) { await say('You have no revives!'); return }
const reviveResult = await reviveTarget(broadcasterId, target, 60 * 60 * 12)
if (reviveResult.status) { await updateInventory(user, itemResult.inv!); await addUsedItem(target!, 'superrevive') }
switch (reviveResult.reason) {
case 'noexist':
await say(`${targetname} does not exist`)
break
case 'notbanned':
await say(`${target?.displayName} doesn't need revives`)
break
case 'unknown':
await say("Something went wrong!")
break
case 'healed':
await say(`${target?.displayName} got healed for 12 hours by ${user.displayName}`)
break
case 'revived':
await say(`${target?.displayName} got revived by ${user.displayName}`)
break
}
}
}

View File

@@ -2,6 +2,8 @@ import { RefreshingAuthProvider, exchangeCode } from '@twurple/auth'
import pb from './pocketbase'
import { RecordModel } from 'pocketbase'
const INTENTS = ['channel:manage:moderators', 'moderation:read', 'moderator:manage:banned_users', 'channel:manage:polls', 'channel:read:polls']
let ttvauth: RecordModel | undefined
try {
ttvauth = await pb.collection('ttvauth').getFirstListItem('main=true')
@@ -28,17 +30,17 @@ async function firstAccess(main = true) {
const CLIENT_SECRET = process.env.CLIENT_SECRET
const OAUTH_CODE = process.env.OAUTH_CODE
const BROADCASTER_OAUTH_CODE = process.env.BROADCASTER_OAUTH_CODE
const REDIRECT_URI = process.env.REDIRECT_URI ?? 'https://qweri0p.github.io/url-params/'
const REDIRECT_URI = process.env.REDIRECT_URI ?? 'https://qwerinope.github.io/url-params/'
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 ((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=${REDIRECT_URI}&response_type=code&scope=chat:read+chat:edit+moderator:manage:banned_users+moderation:read+channel:manage:polls`)
console.error(`https://id.twitch.tv/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=chat:read+chat:edit+${INTENTS.join('+')}`)
} 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=${REDIRECT_URI}&response_type=code&scope=moderator:manage:banned_users+moderation:read+channel:manage:moderators+channel:manage:polls`)
console.error(`https://id.twitch.tv/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=${INTENTS.join('+')}`)
}
process.exit(1)
}
@@ -69,7 +71,7 @@ await authProvider.addUserForToken({
refreshToken: auth.REFRESH_TOKEN,
expiresIn: auth.EXPIRESIN,
obtainmentTimestamp: auth.OBTAINMENTTIMESTAMP
}, ['chat', 'moderator:manage:banned_users', 'channel:manage:polls', 'channel:manage:moderators'])
}, ['chat', ...INTENTS])
authProvider.onRefresh(async (_id, newTokenData) => {
auth.ACCESS_TOKEN = newTokenData.accessToken
@@ -95,7 +97,7 @@ const broadcasterAuthProvider = broadcasterAuthData === undefined ? undefined :
refreshToken: broadcasterAuthData.REFRESH_TOKEN,
expiresIn: broadcasterAuthData.EXPIRESIN,
obtainmentTimestamp: broadcasterAuthData.OBTAINMENTTIMESTAMP
}, ['moderator:manage:banned_users', 'moderation:read', 'channel:manage:moderators', 'channel:manage:polls'])
}, INTENTS)
broadcasterAuthProvider.onRefresh(async (_id, newTokenData) => {
broadcasterAuthData.ACCESS_TOKEN = newTokenData.accessToken

View File

@@ -1,17 +1,17 @@
import { HelixUser } from "@twurple/api"
import { changeBalance, getInventory, updateInventory } from "../lib/userHelper"
import { timeout, addTimeoutToDB, vulnerableUsers } from "./timeoutHelper"
import api, { broadcasterApi } from "./api"
export const ITEMS = ['blaster', 'silverbullet', 'grenade', 'tnt', 'clipboard', 'lootbox']
import { getInventory, inventory, updateInventory } from "../lib/userHelper"
import { ITEMS } from "../items"
interface itemChangeResult {
result: boolean,
reason: string
count: number,
inv?: inventory
}
export async function changeItemCount(user: HelixUser, item: string, amount = -1): Promise<itemChangeResult> {
/** Check if the target user can use/lose item(s) and return the new inventory
* @param [amount=-1] If not specified, reduce count by one
* @param [preconfirmed=false] If it is confirmed that the change is allowed, update the inventory immediately */
export async function changeItemCount(user: HelixUser, item: string, amount = -1, preconfirmed = false): Promise<itemChangeResult> {
if (!ITEMS.includes(item)) return { result: false, reason: 'noexist', count: 0 }
let inv = await getInventory(user)
@@ -21,141 +21,14 @@ export async function changeItemCount(user: HelixUser, item: string, amount = -1
Object.defineProperty(inv, item, {
value: newcount,
})
await updateInventory(user, inv)
return { result: true, reason: '', count: inv[item] }
if (amount > 0 || preconfirmed === true) await updateInventory(user, inv)
return { result: true, reason: '', count: inv[item], inv }
}
export async function useBlaster(broadcasterId: string, attacker: HelixUser, targetname: string, say: (arg0: string) => Promise<void>) {
const target = await api.users.getUserByName(targetname)
export let ITEMBUSY = false
const itemResult = await changeItemCount(attacker, 'blaster')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no blasters mandoooYikes'); return }
const result = await timeout(broadcasterId, target!, 60, `You got blasted by ${attacker.name}`)
if (result.status) {
await say(`${targetname} got blasted by ${attacker.name}! mandoooGOTTEM ${attacker.name} has ${itemResult.count} blaster${itemResult.count === 1 ? '' : 's'} remaining`)
await addTimeoutToDB(attacker, target!, 'blaster')
} else {
switch (result.reason) {
case 'noexist':
await say(`${targetname} doesn't exist!`)
break
case 'banned':
await say(`${targetname} is already dead!`)
break
case 'unknown':
await say(`NO!`)
await timeout(broadcasterId, attacker, 60, "NO!")
break
}
}
}
export async function useSilverBullet(broadcasterId: string, attacker: HelixUser, targetname: string, say: (arg0: string) => Promise<void>) {
const target = await api.users.getUserByName(targetname)
const itemResult = await changeItemCount(attacker, 'silverbullet')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no silver bullets mandoooYikes'); return }
const result = await timeout(broadcasterId, target!, 60 * 60 * 24, `You got hit by a silver bullet fired by ${attacker.name}`)
if (result.status) {
await say(`${target?.name} mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute mandoooSalute `)
await addTimeoutToDB(attacker, target!, 'silverbullet')
} else {
switch (result.reason) {
case 'noexist':
await say(`${targetname} doesn't exist!`)
break
case 'banned':
await say(`${targetname} is already dead!`)
break
case 'unknown':
await say(`NO!`)
await timeout(broadcasterId, attacker, 60, "NO!")
break
}
}
}
export async function useGrenade(broadcasterId: string, attacker: HelixUser, say: (arg0: string) => Promise<void>) {
if (vulnerableUsers.length === 0) { await say('No chatters to blow up!'); return }
const itemResult = await changeItemCount(attacker, 'grenade')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no grenades mandoooYikes'); return }
const target = await api.users.getUserById(vulnerableUsers[Math.floor(Math.random() * vulnerableUsers.length)])
const result = await timeout(broadcasterId, target!, 60, `You got hit by ${attacker.name}'s grenade`)
if (result.status) {
await say(`${target?.name} got blown up by ${attacker.name}'s grenade! mandoooGOTTEM`)
await addTimeoutToDB(attacker, target!, 'grenade')
} else {
// Banned is not an option, and neither is noexist
await say(`something went wrong mandoooYikes`)
console.error(result.reason)
}
}
export async function useTNT(broadcasterId: string, attacker: HelixUser, say: (args0: string) => Promise<void>) {
if (vulnerableUsers.length === 0) { await say('No chatters to blow up!'); return }
const itemResult = await changeItemCount(attacker, 'tnt')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no TNT mandoooYikes'); return }
const min = vulnerableUsers.length < 3 ? vulnerableUsers.length : 3 //if less than 3 chatters, use that else 3
const max = vulnerableUsers.length > 10 ? 10 : vulnerableUsers.length //if more than 10 chatters do 10 else 10
const blastedusers = Math.floor(Math.random() * (max - min + 1)) + min
const soontobedeadusers = shuffle(vulnerableUsers).slice(vulnerableUsers.length - blastedusers)
const targets = await api.users.getUsersByIds(soontobedeadusers)
for (const target of targets) {
const result = await timeout(broadcasterId, target!, 60, `You got hit by ${attacker.name}'s TNT`)
if (result.status) {
await say(`${target?.name} got blown up by TNT! mandoooTNT`)
await addTimeoutToDB(attacker, target!, 'tnt')
} else {
await say(`something went wrong mandoooYikes`)
console.error(result.reason)
}
}
await say(`${attacker.name} blew up ${blastedusers} chatters with their TNT mandoooGOTTEM ${attacker.name} has ${itemResult.count} tnt${itemResult.count === 1 ? '' : 's'} remaining`)
}
function getRandom(): number {
return Math.floor(Math.random() * 100)
}
export async function useLootbox(user: HelixUser, say: (arg0: string) => Promise<void>) {
const itemResult = await changeItemCount(user, 'lootbox')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no lootboxes mandoooYikes'); return }
// Lootbox logic will for now just be get 25 qbucks, with 50% chance to get a grenade 25% chance to get a blaster and 10% chance to get TNT
let inventory = await getInventory(user)
let newitems: string[] = []
await changeBalance(user, 25)
newitems.push('25 qbucks')
if (getRandom() <= 50) { newitems.push('1 grenade'); inventory.grenade += 1 }
if (getRandom() <= 25) { newitems.push('1 blaster'); inventory.blaster += 1 }
if (getRandom() <= 10) { newitems.push('1 tnt'); inventory.tnt += 1 }
await updateInventory(user, inventory)
await say(`${user.name} got: ${newitems.join(' and ')}`)
}
function shuffle(arrayold: any[]) {
let array = arrayold
let currentIndex = array.length;
while (currentIndex != 0) {
let randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
}
return array
}
export async function useClipboard(broadcasterId: string, user: HelixUser, question: string, say: (arg0: string) => Promise<void>) {
const tempapi = broadcasterApi ?? api
const polldata = await tempapi.polls.getPolls(broadcasterId)
if (polldata.data.length > 0) {await say('Can\'t have two polls active at once.'); return}
const itemResult = await changeItemCount(user, 'clipboard')
if (!itemResult.result && itemResult.reason === 'negative') { await say('You have no clipboards mandoooYikes'); return }
await tempapi.polls.createPoll(broadcasterId, { choices: ['Yes', 'No'], duration: 120, title: question })
await say(`${user.name} used a clipboard! They have ${itemResult.count} clipboard${itemResult.count === 1 ? '' : 's'} remaining`)
export function toggleBusy() {
ITEMBUSY = !ITEMBUSY
}

View File

@@ -1,22 +1,22 @@
import { HelixUser } from "@twurple/api"
import pb, { User } from "./pocketbase"
// const COOLDOWN = 1000 * 60 * 60 * 24 * 30 // 1000 milliseconds * 60 seconds * 60 minutes * 24 hours * 30 days
export const COOLDOWN = 1000 * 60 * 15
export const COOLDOWN = (!process.env.COOLDOWN ? 60 * 60 * 24 : Number(process.env.COOLDOWN)) * 1000
interface lootboxReadyResult {
result: boolean,
lastlootbox: number,
DBuser: User
}
export async function lootboxReady(user: HelixUser | null): Promise<lootboxReadyResult> {
const DBuser = await pb.collection('users').getFirstListItem(`twitchid="${user!.id}"`)
/** Check if the lootbox is ready for specified user */
export async function lootboxReady(user: HelixUser): Promise<lootboxReadyResult> {
const DBuser = await pb.collection('users').getFirstListItem(`id="${user!.id}"`)
if ((Date.parse(DBuser.lastlootbox) + COOLDOWN) > Date.now()) return { result: false, lastlootbox: Date.parse(DBuser.lastlootbox), DBuser }
return { result: true, lastlootbox: 0, DBuser }
}
export async function resetLootboxTimer(user: any) {
/** Set the time for last time user got lootbox to now */
export async function resetLootboxTimer(user: User) {
const data = { lastlootbox: new Date(Date.now()).toISOString() }
await pb.collection('users').update(user.id, data)
}

View File

@@ -2,7 +2,6 @@ import PocketBase, { BaseModel, RecordService } from 'pocketbase'
import { inventory } from "./userHelper"
export interface User extends BaseModel {
twitchid: string,
firstname: string,
inventory: inventory,
itemuses: inventory,
@@ -23,11 +22,17 @@ export interface Timeout extends BaseModel {
targetname: string
}
export interface UsedItem extends BaseModel {
name: string,
user: string
}
interface TypedPocketBase extends PocketBase {
collection(idOrName: string): RecordService,
collection(idOrName: 'users'): RecordService<User>,
collection(idOrName: 'ttvauth'): RecordService<TTVAuth>
collection(idOrName: 'timeouts'): RecordService<Timeout>
collection(idOrName: 'itemuses'): RecordService<UsedItem>
}

View File

@@ -1,7 +1,11 @@
import { ApiClient, HelixUser } from "@twurple/api";
import api, { broadcasterApi } from "./api";
import pb from "./pocketbase";
import { getDBID } from "./userHelper";
import { DBValidation } from "./userHelper";
const MODSSTRING = process.env.MODS
if (!MODSSTRING) { console.error("Please set the MODS environment variable."); process.exit(1) }
export const MODS = MODSSTRING.split(',')
type shooter = 'blaster' | 'grenade' | 'silverbullet' | 'tnt'
@@ -10,18 +14,22 @@ interface statusmessage {
reason: string
}
export async function timeout(broadcasterid: string, target: HelixUser, duration: number, reason: string): Promise<statusmessage> {
/** Ban a specific user out by another user for specified amout of time, with specific reason
* If the user does not exist or is already banned return status: false
* If the user is a moderator, make sure they get their status back after timeout has elapsed */
export async function timeout(broadcasterid: string, target: HelixUser | null, duration: number, reason: string): Promise<statusmessage> {
if (!target) return { status: false, reason: 'noexist' }
const tmpapi = broadcasterApi ?? api
// if (target.name === 'qwerinope') return { status: false, reason: 'unknown' }
if (await tmpapi.moderation.checkUserBan(broadcasterid, target)) return { status: false, reason: 'banned' }
if (target.name === process.env.BOT_NAME) return { status: false, reason: 'unknown' }
if (await tmpapi.moderation.checkUserBan(broadcasterid, target.id)) return { status: false, reason: 'banned' }
try {
if (await tmpapi.moderation.checkUserMod(broadcasterid, target)) {
await tmpapi.moderation.removeModerator(broadcasterid, target)
remodMod(broadcasterid, target, duration, tmpapi)
if (await tmpapi.moderation.checkUserMod(broadcasterid, target.id)) {
await tmpapi.moderation.removeModerator(broadcasterid, target.id)
remodMod(broadcasterid, target, duration * 1000, tmpapi)
}
await tmpapi.moderation.banUser(broadcasterid, { duration, reason, user: target })
await tmpapi.moderation.banUser(broadcasterid, { duration, reason, user: target.id })
await DBValidation(target)
return { status: true, reason: '' }
} catch (err) {
console.error(err)
@@ -29,25 +37,55 @@ export async function timeout(broadcasterid: string, target: HelixUser, duration
}
}
/** Revive a specific target for a certain amount of time */
export async function reviveTarget(broadcasterId: string, target: HelixUser | null, duration: number): Promise<statusmessage> {
if (!target) return { status: false, reason: 'noexist' }
const tmpapi = broadcasterApi ?? api
const bandata = await tmpapi.moderation.getBannedUsers(broadcasterId, { userId: target.id })
if (!bandata.data[0]) return { status: false, reason: 'notbanned' }
const newduration = Math.floor((Date.parse(bandata.data[0].expiryDate?.toString()!) - Date.now()) / 1000 - duration) // (timestamp to freedom - current timestamp) / 1000 (to seconds) - duration
try {
if (newduration < 3) { // If the target is going to be unbanned in duration + 3 seconds, unban them anyway
await tmpapi.moderation.unbanUser(broadcasterId, target)
if (MODS.includes(target.name)) remodMod(broadcasterId, target, 0, tmpapi)
return { status: true, reason: 'revived' }
} else {
await tmpapi.moderation.banUser(broadcasterId, { duration: newduration, reason: bandata.data[0].reason!, user: target.id })
if (MODS.includes(target.name)) remodMod(broadcasterId, target, newduration * 1000, tmpapi)
return { status: true, reason: 'healed' }
}
} catch (err) {
console.error(err)
return { status: false, reason: 'unknown' }
}
}
/** Add an entry to the timeouts table */
export async function addTimeoutToDB(attacker: HelixUser, target: HelixUser, source: shooter) {
// This has passed the existance check so there's no need to check if the users exist (twitch)
const attackerDB = await getDBID(attacker)
const targetDB = await getDBID(target)
const timeoutobj = {
source,
attacker: attackerDB,
target: targetDB,
attacker: attacker.id,
target: target.id,
attackername: attacker.name,
targetname: target.name
}
await pb.collection('timeouts').create(timeoutobj)
}
/** Give the target mod status back after timeout */
function remodMod(broadcasterid: string, target: HelixUser, duration: number, api: ApiClient) {
setTimeout(async () => {
await api.moderation.addModerator(broadcasterid, target)
}, (duration + 3) * 1000)
const bandata = await api.moderation.getBannedUsers(broadcasterid, { userId: target.id })
if (bandata.data.length !== 0) {
const timeoutleft = Date.parse(bandata.data[0].expiryDate?.toString()!) - Date.now() // date when timeout expires - current date + 3 seconds constant
remodMod(broadcasterid, target, timeoutleft, api) // Call the current function with new time (recursion)
} else { // If user is still timed out it doesn't try to remod the target
try {
await api.moderation.addModerator(broadcasterid, target.id)
} catch (err) { } // This triggers when the timeout got shortened. try/catch so no runtime error
}
}, duration + 3000) // callback gets called after duration of timeout + 3 seconds
}
export let vulnerableUsers: string[] = []

View File

@@ -1,27 +1,14 @@
import pb, { User } from './pocketbase'
import { HelixUser } from '@twurple/api'
import itemData, { ITEMS } from '../items'
export const EMPTYINV: inventory = {
version: 1,
const EMPTYINV = itemData.reduce((acc, item) => {
acc[item.name] = 0
return acc
}, {} as Record<string, number>)
blaster: 0,
grenade: 0,
silverbullet: 0,
tnt: 0,
clipboard: 0,
lootbox: 0
}
export async function getDBID(user: HelixUser) {
try {
const DBuser = await pb.collection('users').getFirstListItem(`twitchid="${user!.id}"`)
return DBuser.id
} catch (error) {
await createUser(user!)
const DBuser = await pb.collection('users').getFirstListItem(`twitchid="${user!.id}"`)
return DBuser.id
}
export type inventory = {
[K in (keyof typeof EMPTYINV)]: number
}
type balanceGetResult = {
@@ -29,9 +16,11 @@ type balanceGetResult = {
data: User
}
/** Ensures that the user exists in the database
* Returns an object with balance as well as the entire user entry from the database */
export async function getBalance(user: HelixUser): Promise<balanceGetResult> {
await DBValidation(user)
const data = await pb.collection('users').getFirstListItem(`twitchid="${user!.id}"`)
const data = await pb.collection('users').getFirstListItem(`id="${user!.id}"`)
return { balance: data.balance, data }
}
@@ -41,6 +30,7 @@ type balanceChangeResult = {
count: number
}
/** Changes the balance of the current user by the requested amount */
export async function changeBalance(user: HelixUser, amount: number): Promise<balanceChangeResult> {
let { balance, data } = await getBalance(user)
if (amount < 0 && balance - amount < 0) return { result: false, reason: 'negative', count: balance }
@@ -50,7 +40,6 @@ export async function changeBalance(user: HelixUser, amount: number): Promise<ba
}
interface timeoutsGetResult {
user: HelixUser,
hit: {
blaster: number, // I'm going to combine blaster, grenade and tnt into one.
silverbullet: number,
@@ -63,11 +52,13 @@ interface timeoutsGetResult {
const BLASTERS = ['blaster', 'grenade', 'tnt']
async function getTimeouts(user: HelixUser): Promise<timeoutsGetResult> {
await DBValidation(user)
const userDBID = await getDBID(user)
const hit = await pb.collection('timeouts').getFullList({ filter: `target="${userDBID}"` })
const shot = await pb.collection('timeouts').getFullList({ filter: `attacker="${userDBID}"` })
/** Get the amount of times the user has (been) shot (by) another user
* The 'blaster' data is all timeouts excluding silver bullets */
export async function getTimeouts(userId: string, monthdata?: string): Promise<timeoutsGetResult> {
let monthquery = ''
if (monthdata) monthquery = ` && created~"${monthdata}"`
const hit = await pb.collection('timeouts').getFullList({ filter: `target="${userId}"${monthquery}` })
const shot = await pb.collection('timeouts').getFullList({ filter: `attacker="${userId}"${monthquery}` })
const blasterhit = hit.filter((item) => BLASTERS.includes(item.source)).length
const silverbullethit = hit.length - blasterhit
@@ -75,7 +66,6 @@ async function getTimeouts(user: HelixUser): Promise<timeoutsGetResult> {
const silverbulletshot = shot.length - blastershot
return {
user,
hit: {
blaster: blasterhit,
silverbullet: silverbullethit
@@ -87,21 +77,22 @@ async function getTimeouts(user: HelixUser): Promise<timeoutsGetResult> {
}
}
export interface inventory {
version: number,
blaster: number,
grenade: number,
silverbullet: number,
tnt: number,
clipboard: number,
lootbox: number
/** Get the amount of grenades and tnt used by specified user
* The monthdata should be something like "2025-01" if specified */
async function getItemUses(userId: string, monthdata?: string): Promise<inventory> {
let monthquery = ''
if (monthdata) monthquery = ` && created~"${monthdata}"`
const items = await pb.collection('itemuses').getFullList({ filter: `user="${userId}"${monthquery}` })
return {
grenade: items.filter((item) => item.name === 'grenade').length,
tnt: items.filter((item) => item.name === 'tnt').length,
}
}
/** Get the inventory of specific user */
export async function getInventory(user: HelixUser): Promise<inventory> {
await DBValidation(user)
const data = await pb.collection('users').getFirstListItem(`twitchid="${user.id}"`)
const data = await pb.collection('users').getFirstListItem(`id="${user.id}"`)
return data.inventory
}
@@ -109,33 +100,48 @@ interface statsGetResult extends timeoutsGetResult {
used: inventory
}
export async function getStats(user: HelixUser): Promise<statsGetResult> {
const { hit, shot } = await getTimeouts(user)
const dbuser = await pb.collection('users').getFirstListItem(`twitchid="${user.id}"`)
return { user, hit, shot, used: dbuser.itemuses }
/** Get the hits, shoot and used item stats from specific user
* The monthdata should be something like "2025-01" if specified */
export async function getStats(user: HelixUser, monthdata?: string): Promise<statsGetResult> {
await DBValidation(user)
const { hit, shot } = await getTimeouts(user.id, monthdata)
const used = await getItemUses(user.id, monthdata)
return { hit, shot, used }
}
/** Update the inventory of the target user with new inventory data */
export async function updateInventory(user: HelixUser, newinv: inventory) {
await DBValidation(user)
const data = await pb.collection('users').getFirstListItem(`twitchid="${user.id}"`)
const data = await pb.collection('users').getFirstListItem(`id="${user.id}"`)
const recordid = data.id
await pb.collection('users').update(recordid, { inventory: newinv })
}
/** Creates a new entry in the useditems table */
export async function addUsedItem(user: HelixUser, item: string) {
await DBValidation(user)
await pb.collection('itemuses').create({ user: user.id, name: item })
}
async function DBValidation(user: HelixUser) {
/** Validate if the HelixUser has an entry in the database
* Add missing inventory items*/
export async function DBValidation(user: HelixUser) {
try {
await pb.collection('users').getFirstListItem(`twitchid="${user.id}"`)
let { inventory } = await pb.collection('users').getFirstListItem(`id="${user.id}"`)
if (Object.keys(inventory).sort().toString() === ITEMS.sort().toString()) return
ITEMS.forEach(key => {
if (!(key in inventory)) inventory[key] = 0
})
await pb.collection('users').update(user.id, { inventory })
} catch (error) {
await createUser(user!)
}
}
/** Create the user in the database */
async function createUser(user: HelixUser) {
const data = {
twitchid: user.id,
id: user.id,
firstname: user.name,
inventory: EMPTYINV,
itemuses: EMPTYINV,
lastlootbox: "1970-01-01 12:00:00.000Z",
balance: 0
}