diff --git a/.gitea/workflows/docker-dogbot.yml b/.gitea/workflows/docker-dogbot.yml deleted file mode 100644 index d12b9ee..0000000 --- a/.gitea/workflows/docker-dogbot.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Build and Publish docker image - -on: - push: - branches: - - 'main' - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - name: Login to container Registry - uses: docker/login-action@v3 - with: - registry: git.qwerinope.com - username: qwerinope - password: ${{ secrets.PAT }} - - name: Get repository - uses: actions/checkout@v4 - - name: Docker Build and push - uses: docker/build-push-action@v6 - with: - file: Dockerfile.dogbot - context: . - push: true - tags: git.qwerinope.com/qwerinope/dogbot-bot:latest diff --git a/.gitea/workflows/docker-pocketbase.yml b/.gitea/workflows/docker-pocketbase.yml deleted file mode 100644 index 700f15f..0000000 --- a/.gitea/workflows/docker-pocketbase.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Build and Publish docker image - -on: - push: - branches: - - 'main' - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - name: Login to container Registry - uses: docker/login-action@v3 - with: - registry: git.qwerinope.com - username: qwerinope - password: ${{ secrets.PAT }} - - name: Get repository - uses: actions/checkout@v4 - - name: Docker Build and push - uses: docker/build-push-action@v6 - with: - file: Dockerfile.pocketbase - context: . - push: true - tags: git.qwerinope.com/qwerinope/dogbot-pocketbase:latest diff --git a/.github/workflows/docker-pocketbase.yml b/.github/workflows/docker-pocketbase.yml index 4e84785..212ac40 100644 --- a/.github/workflows/docker-pocketbase.yml +++ b/.github/workflows/docker-pocketbase.yml @@ -1,4 +1,4 @@ -name: Build and Publish pocketbase docker image +name: Build and Publish qweribot-pocketbase docker image on: push: @@ -23,5 +23,5 @@ jobs: file: Dockerfile.pocketbase context: . push: true - tags: ghcr.io/qweri0p/dogbot-pocketbase:latest + tags: ghcr.io/qwerinope/qweribot-pocketbase:latest secrets: "github_token=${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/docker-dogbot.yml b/.github/workflows/docker-qweribot.yml similarity index 80% rename from .github/workflows/docker-dogbot.yml rename to .github/workflows/docker-qweribot.yml index 4d6949e..20ebb9a 100644 --- a/.github/workflows/docker-dogbot.yml +++ b/.github/workflows/docker-qweribot.yml @@ -1,4 +1,4 @@ -name: Build and Publish dogbot docker image +name: Build and Publish qweribot docker image on: push: @@ -20,8 +20,8 @@ jobs: - name: Docker Build and push uses: docker/build-push-action@v6 with: - file: Dockerfile.dogbot + file: Dockerfile.qweribot context: . push: true - tags: ghcr.io/qweri0p/dogbot-bot:latest + tags: ghcr.io/qwerinope/qweribot-bot:latest secrets: "github_token=${{ secrets.GITHUB_TOKEN }}" diff --git a/Dockerfile.dogbot b/Dockerfile.qweribot similarity index 100% rename from Dockerfile.dogbot rename to Dockerfile.qweribot diff --git a/README.md b/README.md index c7f484b..23a9e22 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,109 @@ -# OpenMandooBot +# qweribot +A copy of twitch bot 'MandooBot' in [eddie's stream](twitch.tv/eddie). + +## Usage + +### Commands + +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` +`!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` +`!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` + +### Items + +Here are the items that can be used. +These can be used with the `!use` command or with their aliases. + +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` +`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` +`clipboard {message}`|Starts a two minute long poll with the user specified message|`!clipboard` + +## Setup + +### Docker compose (recommended) + +Docker compose is the only recommended way to use qweribot as it sets up pocketbase and the bot automatically. +The `compose.yaml` file in the repository is for development. + +```yaml +services: + qweribot: + container_name: qweribot + image: ghcr.io/qwerinope/qweribot: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 + # 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 + # 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 + # Make sure that CLIENT_ID, CLIENT_SECRET and REDIRECT_URI are still set when enabling DIFFERENT_BROADCASTER after first setup + restart: no + depends_on: + pocketbase: + condition: service_started + + pocketbase: + container_name: pocketbase + image: ghcr.io/qwerinope/pocketbase-qweribot: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: + # - EMAIL=test@example.com + # - PASSWORD=1234567890 + restart: no + ports: + - 8090:8090 + volumes: + # Make sure that the data in the container at /pb/pb_data is persistent + - ./data:/pb/pb_data +``` + +### 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. +Also make sure the migration from `pb/migrations` is passed into the pocketbase instance. + +Install dependencies with `bun install`. +Run the bot with `bun run src/bot.ts` ## Configuration -VARIABLE|DEFAULT|FUNCTION --|-|- -`BOT_NAME`|None|Set the name of the bot user for Authentification -`CHANNEL`|None| Set the name of the twitch channel to join -`CLIENT_ID`|None|Set the CLIENT_ID to authenticate the bot (ignored after setup) -`CLIENT_SECRET`|None|Set the CLIENT_SECRET to authenticate the bot (ignored after setup) -`REDIRECT_URI`|`https://qweri0p.github.io/url-params/`|The REDIRECT_URI set in the twitch dev console (optional after setup) -`OAUTH_CODE`|None|Authorization code for OAuth (ignored after setup) -`DIFFERENT_BROADCASTER`|`false`|Set this to true when `BOT_NAME` and `CHANNEL` are different. -`BROADCASER_OAUTH_CODE`|None|OAuth authorization code for the broadcaster (ignored if `DIFFERENT_BROADCASTER` is false) (optional after setup) -`PBURL`|`http://pocketbase:8090`|Where the pocketbase database is found -`EMAIL`|`test@example.com`|Pocketbase Admin UI email used for login (ignored after setup) -`PASSWORD`|`1234567890`|Pocketbase Admin UI password used for login (ignored after setup) +These are the environment variables that can be used to set up the bot. +Options with :bangbang: in the Required column need to be present for setup, and can be removed afterwards. + +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: +`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: +`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: +`PBURL`|`http://pocketbase:8090`|Where the pocketbase database is found|:x: +`EMAIL`|`test@example.com`|Pocketbase Admin UI email used for login|:x: +`PASSWORD`|`1234567890`|Pocketbase Admin UI password used for login|:x: diff --git a/compose.yml b/compose.yml index 6d5ef2c..e9307ca 100644 --- a/compose.yml +++ b/compose.yml @@ -18,10 +18,10 @@ services: depends_on: pocketbase: condition: service_started - container_name: dogbot + container_name: qweribot build: context: . - dockerfile: Dockerfile.dogbot + dockerfile: Dockerfile.qweribot restart: no develop: watch: diff --git a/src/commands/admin.ts b/src/commands/admin.ts index e79fe8c..fb9ba57 100644 --- a/src/commands/admin.ts +++ b/src/commands/admin.ts @@ -4,7 +4,7 @@ import { changeItemCount } from "../lib/items"; import { changeBalance } from "../lib/userHelper"; import { vulnerableUsers } from "../lib/timeoutHelper"; -const give = createBotCommand('give', async (params, { say, broadcasterId, userId }) => { +const give = createBotCommand('give', async (params, { say, broadcasterId, userId, userName }) => { if (userId !== broadcasterId) return const target = await api.users.getUserByName(params[0].replace(/[@]/g, '')) @@ -12,7 +12,7 @@ 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() === 'mbucks' ? 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])) 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 } @@ -20,7 +20,7 @@ const give = createBotCommand('give', async (params, { say, broadcasterId, userI await say(`${target.name} now has ${data.count} ${params[1]}`) }) -const vulnChatters = createBotCommand('vulnchatters', async (_params, { say, userId, broadcasterId }) => { +const vulnChatters = createBotCommand('vulnchatters', async (_params, { say, userId, broadcasterId, userName }) => { if (userId !== broadcasterId) return await say(`There are ${vulnerableUsers.length} vulnerable chatters`) diff --git a/src/commands/getloot.ts b/src/commands/getloot.ts index d889732..3339398 100644 --- a/src/commands/getloot.ts +++ b/src/commands/getloot.ts @@ -3,7 +3,7 @@ import { COOLDOWN, lootboxReady, resetLootboxTimer } from "../lib/lootboxes"; import { changeItemCount } from "../lib/items" import api from "../lib/api" -function getTimeDifference(date1: number , date2: number) { +function getTimeDifference(date1: number, date2: number) { const diff = Math.abs(date1 - date2); const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); @@ -13,9 +13,10 @@ 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, broadcasterId }) => { const user = await api.users.getUserById(userId) - // if (!user?.isSubscribedTo(broadcasterId)) {await reply('Subscribe to get loot mandoooSmile'); return} + // 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) if (!data.result) { const { days, hours, minutes, seconds } = getTimeDifference(data.lastlootbox, Date.now() - COOLDOWN) @@ -27,7 +28,7 @@ export default createBotCommand('getloot', async (_params, { reply, userId/*, br `) return } - await reply(`You got a lootbox mandoooSmile`) + await reply(`You got a lootbox :)`) await changeItemCount(user!, 'lootbox', 1) await resetLootboxTimer(data.DBuser) }) diff --git a/src/commands/index.ts b/src/commands/index.ts index 2928c8d..1a8edf7 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,13 +1,13 @@ import timeout from "./timeout"; -import thank from "./thank" import inventory from "./inventory"; import stats from "./stats"; -import mbucks from "./mbucks"; +import qbucks from "./qbucks"; import getloot from "./getloot"; import modme from "./modme"; import use from "./use"; +import iteminfo from "./iteminfo" import aliases from './itemAliases' import admin from './admin' -export default [timeout, thank, inventory, stats, mbucks, getloot, modme, use, ...aliases, ...admin] +export default [timeout, inventory, stats, qbucks, getloot, modme, use, iteminfo, ...aliases, ...admin] diff --git a/src/commands/iteminfo.ts b/src/commands/iteminfo.ts new file mode 100644 index 0000000..81a59ee --- /dev/null +++ b/src/commands/iteminfo.ts @@ -0,0 +1,31 @@ +import { createBotCommand } from "@twurple/easy-bot"; + +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) +}, { aliases: ['item'] }) diff --git a/src/commands/mbucks.ts b/src/commands/qbucks.ts similarity index 72% rename from src/commands/mbucks.ts rename to src/commands/qbucks.ts index 48574e4..9940fd6 100644 --- a/src/commands/mbucks.ts +++ b/src/commands/qbucks.ts @@ -3,10 +3,10 @@ import { HelixUser } from "@twurple/api"; import api from "../lib/api"; import { getBalance } from "../lib/userHelper"; -export default createBotCommand('mbucks', async (params, { userName, say }) => { +export default createBotCommand('balance', async (params, { userName, say }) => { 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`) @@ -14,6 +14,6 @@ export default createBotCommand('mbucks', async (params, { userName, say }) => { } const data = await getBalance(user) - await say(`${user.name} has ${data.balance} mbucks ${data.balance === 0 ? 'mandoooYikes' : 'mandoooSmile'}`) + await say(`${user.name} has ${data.balance} qbucks ${data.balance === 0 ? 'mandoooYikes' : 'mandoooSmile'}`) -}, { aliases: ['mbux', 'mandoobucks'] }) +}, { aliases: ['qbucks', 'qweribucks', 'bal'] }) diff --git a/src/commands/thank.ts b/src/commands/thank.ts deleted file mode 100644 index a0812ae..0000000 --- a/src/commands/thank.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createBotCommand } from "@twurple/easy-bot"; - -export default createBotCommand("thank", async (params, { say, msg }) => { - if (params.length === 0) { await say(`chumpi4Heart ${msg.userInfo.userName}`); return } - await say(`chumpi4Heart ${params.join(' ')}`) -}) diff --git a/src/commands/timeout.ts b/src/commands/timeout.ts index 5895403..13fc4e0 100644 --- a/src/commands/timeout.ts +++ b/src/commands/timeout.ts @@ -6,12 +6,12 @@ import api from "../lib/api"; export default createBotCommand('timeout', async (params, { say, broadcasterId, userName }) => { const attacker = await api.users.getUserByName(userName) const userbal = await getBalance(attacker!) - if (userbal.balance < 100) { await say('not enough mandoobucks'); return } + if (userbal.balance < 100) { await say('not enough qbucks'); return } if (params.length === 0) { await say("nice miss bro"); return } - const target = await api.users.getUserByName(params[0].replace(/[^a-zA-Z0-9]/g, '')) + 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 mandoooGun by ${userName}! mandoooGOTTEM ${userName} now has ${userbal.balance - 100} mandoobucks remaining`) + await say(`${params[0]} got blasted by ${userName}! mandoooGOTTEM ${userName} now has ${userbal.balance - 100} qbucks remaining`) await changeBalance(attacker!, -100) await addTimeoutToDB(attacker!, target!, 'blaster') } diff --git a/src/lib/items.ts b/src/lib/items.ts index 45cc575..6e07587 100644 --- a/src/lib/items.ts +++ b/src/lib/items.ts @@ -34,7 +34,7 @@ export async function useBlaster(broadcasterId: string, attacker: HelixUser, tar const result = await timeout(broadcasterId, target!, 60, `You got blasted by ${attacker.name}`) if (result.status) { - await say(`${targetname} got mandoooGun by ${attacker.name}! mandoooGOTTEM ${attacker.name} has ${itemResult.count} blaster${itemResult.count === 1 ? '' : 's'} remaining`) + 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) { @@ -126,11 +126,11 @@ function getRandom(): number { export async function useLootbox(user: HelixUser, say: (arg0: string) => Promise) { 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 mbucks, with 50% chance to get a grenade 25% chance to get a blaster and 10% chance to get TNT + // 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 mbucks') + 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 }