add sound alerts, add more point redeem functions

This commit is contained in:
2025-09-25 16:56:43 +02:00
parent e6e82b6cf2
commit 76bf3d34a8
14 changed files with 154 additions and 8 deletions

View File

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

View File

@@ -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`);
}
});

View File

@@ -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`);
}
});

View File

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

View File

@@ -19,7 +19,7 @@ export default class PointRedeem {
public readonly color?: string;
public readonly execute: (message: EventSubChannelRedemptionAddEvent, sender: User) => Promise<void>;
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<number, PointRedeem>;
/** A map of all (including inactive) redeems mapped to names */
const namedRedeems = new Map<string, PointRedeem>;
@@ -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<string, PointRedeem>;
/** Map of redeemname to twitch redeem ID */
const idMap = new Map<string, string>;
import { streamerApi, streamerId } from "main";
import logger from "lib/logger";
const currentRedeems = new Map<string, string>;
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 };

View File

@@ -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'
})
});

View File

@@ -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'
})
});

View File

@@ -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'
})
});

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -0,0 +1,8 @@
import { soundAlert } from "web/alerts/types";
import { AlertRunner } from "./index";
export default async function execute(alert: soundAlert): Promise<AlertRunner> {
const audio = new Audio(`/alerts/public/${alert.sound}.ogg`);
audio.play();
return { blocking: false, duration: 1, alertDiv: document.createElement('div') };
};