diff --git a/README.md b/README.md index c346254..bee417b 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,16 @@ ITEM|RATE Each of these rates get pulled 3 times, then the result is added to your inventory. It's theoretically possible to get 3 of each item. +### Redeems + +Redeems will be created automatically when the bot starts. + +Redeems or Pointredeems are events/commands triggered when a chatter uses their channel points. +Redeems can be enabled and disabled by moderators using the [`enableredeem` and `disableredeem` commands](#administrative-commands). +Note: The commands mentioned above require the internal name. For example, to disable the free money redeem, you do `disableredeem qbucksredeem` and not `disableredeem FREE MONEY`. + +When running the development database and twitch api application, the redeems will not get created. This is because twitch only allows editing or deleting rewards when the same application created the reward. (fucking stupid) + ### Chatterbot/streamerbot This depends on if the `CHATTER_IS_STREAMER` environment variable is set. @@ -145,6 +155,8 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE `enablecommand {command/item}`|Re-enable a specific command/item|moderator|`enablecommand`|:x: `disablecheer {cheer}`|Disable a specific cheer event|moderator|`disablecheer`|:x: `enablecheer {cheer}`|Re-enable a specific cheer event|moderator|`enablecheer`|:x: +`disableredeem {internalredeemname}`|Disable a specific channel point redemption [(info)](#redeems)|moderator|`disableredeem`|:x: +`enableredeem {internalredeemname}`|Enable a specific channel point redemption [(info)](#redeems)|moderator|`enableredeem`|:x: `getinvulns`|Get a list of every invulnerable chatter in the channel|anyone|`getinvulns`|:x: `getadmins`|Get a list of every admin in the channel|anyone|`getadmins`|:x: `itemlock {target}`|Toggle the itemlock on the specified target|moderator|`itemlock`|:x: @@ -173,3 +185,12 @@ NAME|AMOUNT|USAGE|FUNCTION `timeout`|100|`cheer100 {target}`|Times specified user out for 1 minute. On failure gives cheerer a blaster `execute`|666|`cheer666 {target}`|Times specified user out for 30 minutes. On failure gives cheerer a silver bullet `tnt`|1000|`cheer1000`|Gives 5-10 random vulnerable chatters 60 second timeouts. On failure gives cheerer a TNT + +## Point Redeems + +NAME|COST|DESCRIPTION|INTERNALNAME +-|-|-|- +`FREE MONEY`|1000|Get 100 qbucks|`qbucksredeem` +`RIPBOZO`|500|Sound: Coffeezilla calls me a conman [(source)](https://www.youtube.com/watch?v=QRvEGn7i-wM)|`sfxripbozo` +`Welcome to the Madhouse`|100|Sound: mrockstar20 says: "Welcome to the Madhouse"|`sfxmrockmadhouse` +`Eddie Scream`|100|Sound: Eddie screams|`sfxeddiescream` diff --git a/src/commands/disableredeem.ts b/src/commands/disableredeem.ts new file mode 100644 index 0000000..bf9eb54 --- /dev/null +++ b/src/commands/disableredeem.ts @@ -0,0 +1,19 @@ +import { Command, sendMessage } from "commands"; +import parseCommandArgs from "lib/parseCommandArgs"; +import { disableRedeem, idMap, namedRedeems } from "pointRedeems"; + +export default new Command({ + name: 'disableRedeem', + aliases: ['disableredeem'], + usertype: 'moderator', + disableable: false, + execution: async msg => { + const args = parseCommandArgs(msg.messageText); + if (!args[0]) { await sendMessage("Please specify a point redemption to disable"); return; }; + const selection = namedRedeems.get(args[0]); + if (!selection) { await sendMessage(`Redeem ${args[0]} doesn't exist. The internal names for redeems are here: https://github.com/qwerinope/qweribot#point-redeems`); return; }; + const id = idMap.get(selection.name); + await disableRedeem(selection, id!); + await sendMessage(`The ${selection.name} point redeem is now disabled`); + } +}); diff --git a/src/commands/enableredeem.ts b/src/commands/enableredeem.ts new file mode 100644 index 0000000..c16b12f --- /dev/null +++ b/src/commands/enableredeem.ts @@ -0,0 +1,19 @@ +import { Command, sendMessage } from "commands"; +import parseCommandArgs from "lib/parseCommandArgs"; +import { enableRedeem, idMap, namedRedeems } from "pointRedeems"; + +export default new Command({ + name: 'enableRedeem', + aliases: ['enableredeem'], + usertype: 'moderator', + disableable: false, + execution: async msg => { + const args = parseCommandArgs(msg.messageText); + if (!args[0]) { await sendMessage("Please specify a point redemption to enable"); return; }; + const selection = namedRedeems.get(args[0]); + if (!selection) { await sendMessage(`Redeem ${args[0]} doesn't exist. The internal names for redeems are here: https://github.com/qwerinope/qweribot#point-redeems`); return; }; + const id = idMap.get(selection.name); + await enableRedeem(selection, id!); + await sendMessage(`The ${selection.name} point redeem is now enabled`); + } +}); diff --git a/src/events/channelPoints.ts b/src/events/channelPoints.ts index a8a464f..dc0cac6 100644 --- a/src/events/channelPoints.ts +++ b/src/events/channelPoints.ts @@ -10,6 +10,7 @@ eventSub.onChannelRedemptionAdd(streamerId, async msg => { const user = await User.initUsername(msg.userName); try { await selection.execute(msg, user!); + if (process.env.NODE_ENV === 'production') await msg.updateStatus('FULFILLED'); // only on prod } catch (err) { await sendMessage(`[ERROR]: Something went wrong with ${user?.displayName}'s redeem!`); logger.err(err as string); diff --git a/src/pointRedeems/index.ts b/src/pointRedeems/index.ts index 3de1564..c900cca 100644 --- a/src/pointRedeems/index.ts +++ b/src/pointRedeems/index.ts @@ -19,7 +19,7 @@ export default class PointRedeem { public readonly color?: string; public readonly execute: (message: EventSubChannelRedemptionAddEvent, sender: User) => Promise; constructor(options: pointRedeemOptions) { - this.name = options.name + this.name = options.name.toLowerCase(); this.title = options.title; this.prompt = options.prompt; this.cost = options.cost; @@ -30,8 +30,6 @@ export default class PointRedeem { import { readdir } from 'node:fs/promises'; -const pointRedeems = new Map; - /** A map of all (including inactive) redeems mapped to names */ const namedRedeems = new Map; @@ -40,7 +38,6 @@ for (const file of files) { if (!file.endsWith('.ts')) continue; if (file === import.meta.file) continue; const redeem: PointRedeem = await import(import.meta.dir + '/' + file.slice(0, -3)).then(a => a.default); - pointRedeems.set(redeem.cost, redeem); namedRedeems.set(redeem.name, redeem); }; @@ -48,15 +45,20 @@ export { namedRedeems }; const activeRedeems = new Map; +/** Map of redeemname to twitch redeem ID */ +const idMap = new Map; + import { streamerApi, streamerId } from "main"; import logger from "lib/logger"; const currentRedeems = new Map; await streamerApi.channelPoints.getCustomRewards(streamerId).then(a => a.map(b => currentRedeems.set(b.title, b.id))); -for (const [_, redeem] of Array.from(pointRedeems)) { +for (const [_, redeem] of Array.from(namedRedeems)) { + if (process.env.NODE_ENV !== 'production') continue; // If created with dev-app we won't be able to change it with prod app const selection = currentRedeems.get(redeem.title); if (selection) { currentRedeems.delete(redeem.title); + idMap.set(redeem.name, selection); activeRedeems.set(selection, redeem); } else { const creation = await streamerApi.channelPoints.createCustomReward(streamerId, { @@ -66,12 +68,34 @@ for (const [_, redeem] of Array.from(pointRedeems)) { backgroundColor: redeem.color }); logger.ok(`Created custom point redeem ${redeem.title}`); + idMap.set(redeem.name, creation.id); activeRedeems.set(creation.id, redeem); }; }; -Array.from(currentRedeems).map(async ([title, redeem]) => { await streamerApi.channelPoints.deleteCustomReward(streamerId, redeem); logger.ok(`Deleted custom point redeem ${title}`); }); +Array.from(currentRedeems).map(async ([title, redeem]) => { + if (process.env.NODE_ENV !== 'production') return; + await streamerApi.channelPoints.deleteCustomReward(streamerId, redeem); logger.ok(`Deleted custom point redeem ${title}`); +}); logger.ok("Successfully synced all custom point redeems"); -export { activeRedeems }; +export async function enableRedeem(redeem: PointRedeem, id: string) { + if (process.env.NODE_ENV !== 'production') return; + await streamerApi.channelPoints.updateCustomReward(streamerId, id, { + isEnabled: true + }); + activeRedeems.set(id, redeem); + logger.ok(`Enabled the ${redeem.name} point redeem`); +}; + +export async function disableRedeem(redeem: PointRedeem, id: string) { + if (process.env.NODE_ENV !== 'production') return; + await streamerApi.channelPoints.updateCustomReward(streamerId, id, { + isEnabled: false + }); + activeRedeems.delete(id); + logger.ok(`Disabled the ${redeem.name} point redeem`); +}; + +export { activeRedeems, idMap }; diff --git a/src/pointRedeems/sfxEddieScream.ts b/src/pointRedeems/sfxEddieScream.ts new file mode 100644 index 0000000..8979ab2 --- /dev/null +++ b/src/pointRedeems/sfxEddieScream.ts @@ -0,0 +1,15 @@ +import PointRedeem from "pointRedeems"; +import { playAlert } from "web/alerts/serverFunctions"; + +export default new PointRedeem({ + name: "sfxEddieScream", + title: "Eddie scream", + cost: 100, + color: "#A020F0", + prompt: "Eddie screaming", + execution: async msg => await playAlert({ + name: 'sound', + user: msg.userDisplayName, + sound: 'eddiescream' + }) +}); diff --git a/src/pointRedeems/sfxMrockMadhouse.ts b/src/pointRedeems/sfxMrockMadhouse.ts new file mode 100644 index 0000000..4c8669e --- /dev/null +++ b/src/pointRedeems/sfxMrockMadhouse.ts @@ -0,0 +1,15 @@ +import PointRedeem from "pointRedeems"; +import { playAlert } from "web/alerts/serverFunctions"; + +export default new PointRedeem({ + name: "sfxMrockMadhouse", + title: "Welcome to the Madhouse", + cost: 100, + color: "#A020F0", + prompt: "mrockstar20 saying 'Welcome to the Madhouse'", + execution: async msg => await playAlert({ + name: 'sound', + user: msg.userDisplayName, + sound: 'mrockmadhouse' + }) +}); diff --git a/src/pointRedeems/sfxRipBozo.ts b/src/pointRedeems/sfxRipBozo.ts new file mode 100644 index 0000000..f95d007 --- /dev/null +++ b/src/pointRedeems/sfxRipBozo.ts @@ -0,0 +1,15 @@ +import PointRedeem from "pointRedeems"; +import { playAlert } from "web/alerts/serverFunctions"; + +export default new PointRedeem({ + name: "sfxRipBozo", + title: "RIP BOZO", + cost: 500, + color: "#A020F0", + prompt: "Coffeezilla calls me a conman", + execution: async msg => await playAlert({ + name: 'sound', + user: msg.userDisplayName, + sound: 'ripbozo' + }) +}); diff --git a/src/web/alerts/types.ts b/src/web/alerts/types.ts index de03dba..d9e55d1 100644 --- a/src/web/alerts/types.ts +++ b/src/web/alerts/types.ts @@ -21,11 +21,18 @@ export type tntExplosionAlert = alertBase<'tntExplosion'> & { targets: string[]; }; +export type soundAlerts = 'mrockmadhouse' | 'eddiescream' | 'ripbozo'; + +export type soundAlert = alertBase<'sound'> & { + sound: soundAlerts; +}; + export type alert = | userBlastAlert | userExecutionAlert | grenadeExplosionAlert - | tntExplosionAlert; + | tntExplosionAlert + | soundAlert; type playAlertEvent = { function: 'playAlert'; diff --git a/src/web/alerts/www/public/eddiescream.ogg b/src/web/alerts/www/public/eddiescream.ogg new file mode 100644 index 0000000..2cb37b1 Binary files /dev/null and b/src/web/alerts/www/public/eddiescream.ogg differ diff --git a/src/web/alerts/www/public/mrockmadhouse.ogg b/src/web/alerts/www/public/mrockmadhouse.ogg new file mode 100644 index 0000000..0160066 Binary files /dev/null and b/src/web/alerts/www/public/mrockmadhouse.ogg differ diff --git a/src/web/alerts/www/public/ripbozo.ogg b/src/web/alerts/www/public/ripbozo.ogg new file mode 100644 index 0000000..dd9ec04 Binary files /dev/null and b/src/web/alerts/www/public/ripbozo.ogg differ diff --git a/src/web/alerts/www/src/alerts/index.ts b/src/web/alerts/www/src/alerts/index.ts index 24dddc4..c17e720 100644 --- a/src/web/alerts/www/src/alerts/index.ts +++ b/src/web/alerts/www/src/alerts/index.ts @@ -3,6 +3,7 @@ import userBlast from "./userBlast"; import userExecution from "./userExecution"; import grenadeExplosion from "./grenadeExplosion"; import tntExplosion from "./tntExplosion"; +import sound from "./sound"; export type AlertRunner = { duration: number; @@ -19,4 +20,5 @@ export default { 'userExecution': userExecution, 'grenadeExplosion': grenadeExplosion, 'tntExplosion': tntExplosion, + 'sound': sound } as AlertMap; diff --git a/src/web/alerts/www/src/alerts/sound.ts b/src/web/alerts/www/src/alerts/sound.ts new file mode 100644 index 0000000..3df7acc --- /dev/null +++ b/src/web/alerts/www/src/alerts/sound.ts @@ -0,0 +1,8 @@ +import { soundAlert } from "web/alerts/types"; +import { AlertRunner } from "./index"; + +export default async function execute(alert: soundAlert): Promise { + const audio = new Audio(`/alerts/public/${alert.sound}.ogg`); + audio.play(); + return { blocking: false, duration: 1, alertDiv: document.createElement('div') }; +};