mirror of
https://github.com/qwerinope/qweribot.git
synced 2025-12-18 23:01:38 +01:00
add whispering messages, add db connection check, add commands alias
This commit is contained in:
10
README.md
10
README.md
@@ -35,6 +35,12 @@ Not all Commands can be disabled, the `DISABLEABLE` field below shows if they ca
|
|||||||
|
|
||||||
A full list of Commands can be found [here](#commands-1)
|
A full list of Commands can be found [here](#commands-1)
|
||||||
|
|
||||||
|
### Timeouts and whispering messages
|
||||||
|
|
||||||
|
If you've been timed out, you can whisper a message to the chatterbot and it will relay your message to the chat.
|
||||||
|
You can only send one message every 10 minutes.
|
||||||
|
Try to bargain for your release with the chatter that shot you, or just call them names.
|
||||||
|
|
||||||
### Items and Itemlock
|
### Items and Itemlock
|
||||||
|
|
||||||
Items are commands that can only be used when the chatter has them in their inventory.
|
Items are commands that can only be used when the chatter has them in their inventory.
|
||||||
@@ -73,6 +79,8 @@ The chatterbot is the user that types in chat. They have very minimal required s
|
|||||||
|
|
||||||
The streamerbot (not that streamerbot) is the broadcaster. This bot needs them to authenticate as well. This account will be used to perform moderation and watch the chat.
|
The streamerbot (not that streamerbot) is the broadcaster. This bot needs them to authenticate as well. This account will be used to perform moderation and watch the chat.
|
||||||
|
|
||||||
|
Using one account as both chatterbot and streamerbot hasn't been tested in a long time. There may be broken features.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### Fun commands
|
### Fun commands
|
||||||
@@ -105,7 +113,7 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
|
|||||||
|
|
||||||
COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
|
COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
|
||||||
-|-|-|-|-
|
-|-|-|-|-
|
||||||
`getcommands [enabled/disabled]`|Get a list of all, enabled or disabled commands|anyone|`getcommands` `getc`|:x:
|
`getcommands [enabled/disabled]`|Get a list of all, enabled or disabled commands|anyone|`commands` `getcommands` `getc`|:x:
|
||||||
`getcheers [enabled/disabled]`|Get a list of all, enabled or disabled cheers|anyone|`getcheers` `getcheer`|:x:
|
`getcheers [enabled/disabled]`|Get a list of all, enabled or disabled cheers|anyone|`getcheers` `getcheer`|:x:
|
||||||
`gettimeout {target}`|Get the remaining timeout duration of targeted user|anyone|`gettimeout` `gett`|:white_check_mark:
|
`gettimeout {target}`|Get the remaining timeout duration of targeted user|anyone|`gettimeout` `gett`|:white_check_mark:
|
||||||
`stacking [on/off]`|Check/set if timeouts are stacking. Only admins can set the stacking state|anyone/admins|`stacking`|:x:
|
`stacking [on/off]`|Check/set if timeouts are stacking. Only admins can set the stacking state|anyone/admins|`stacking`|:x:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import parseCommandArgs from "lib/parseCommandArgs";
|
|||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'getcommands',
|
name: 'getcommands',
|
||||||
aliases: ['getcommands', 'getc'],
|
aliases: ['getcommands', 'getc', 'commands'],
|
||||||
usertype: 'chatter',
|
usertype: 'chatter',
|
||||||
disableable: false,
|
disableable: false,
|
||||||
execution: async msg => {
|
execution: async msg => {
|
||||||
|
|||||||
24
src/connectionCheck.ts
Normal file
24
src/connectionCheck.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import pocketbase from "db/connection";
|
||||||
|
import { RedisClient } from "bun";
|
||||||
|
import logger from "lib/logger";
|
||||||
|
|
||||||
|
export async function connectionCheck() {
|
||||||
|
let pbstatus = false;
|
||||||
|
try {
|
||||||
|
await pocketbase.health.check().then(a => a.code === 200);
|
||||||
|
pbstatus = true;
|
||||||
|
} catch { };
|
||||||
|
const tempclient = new RedisClient(undefined, {
|
||||||
|
connectionTimeout: 100,
|
||||||
|
maxRetries: 1,
|
||||||
|
});
|
||||||
|
let redisstatus = false;
|
||||||
|
try {
|
||||||
|
await tempclient.connect();
|
||||||
|
redisstatus = true;
|
||||||
|
} catch { };
|
||||||
|
logger.info(`Currently using the "${process.env.NODE_ENV ?? "production"}" database`);
|
||||||
|
pbstatus ? logger.ok(`Pocketbase status: good`) : logger.err(`Pocketbase status: bad`);
|
||||||
|
redisstatus ? logger.ok(`Redis/Valkey status: good`) : logger.err(`Redis/Valkey status: bad`);
|
||||||
|
if (!pbstatus || !redisstatus) process.exit(1);
|
||||||
|
};
|
||||||
98
src/events/handleSubscriptions.ts
Normal file
98
src/events/handleSubscriptions.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { eventSub, chatterEventSub, streamerApi, streamerId, chatterApi, chatterId } from "main";
|
||||||
|
import { HelixEventSubSubscription } from "@twurple/api";
|
||||||
|
import kleur from "kleur";
|
||||||
|
import logger from "lib/logger";
|
||||||
|
|
||||||
|
// This file is such a fucking disaster lmaooooo
|
||||||
|
|
||||||
|
eventSub.onRevoke(event => {
|
||||||
|
logger.ok(`Successfully revoked streamer EventSub subscription: ${kleur.underline(event.id)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSub.onSubscriptionCreateSuccess(event => {
|
||||||
|
logger.ok(`Successfully created streamer EventSub subscription: ${kleur.underline(event.id)}`);
|
||||||
|
deleteDuplicateStreamerSubscriptions.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSub.onSubscriptionCreateFailure(event => {
|
||||||
|
logger.err(`Failed to create streamer EventSub subscription: ${kleur.underline(event.id)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSub.onSubscriptionDeleteSuccess(event => {
|
||||||
|
logger.ok(`Successfully deleted streamer EventSub subscription: ${kleur.underline(event.id)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSub.onSubscriptionDeleteFailure(event => {
|
||||||
|
logger.err(`Failed to delete streamer EventSub subscription: ${kleur.underline(event.id)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
chatterEventSub.onRevoke(event => {
|
||||||
|
logger.ok(`Successfully revoked chatter EventSub subscription: ${kleur.underline(event.id)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
chatterEventSub.onSubscriptionCreateSuccess(event => {
|
||||||
|
logger.ok(`Successfully created chatter EventSub subscription: ${kleur.underline(event.id)}`);
|
||||||
|
deleteDuplicateChatterSubscriptions.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
chatterEventSub.onSubscriptionCreateFailure(event => {
|
||||||
|
logger.err(`Failed to create chatter EventSub subscription: ${kleur.underline(event.id)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
chatterEventSub.onSubscriptionDeleteSuccess(event => {
|
||||||
|
logger.ok(`Successfully deleted chatter EventSub subscription: ${kleur.underline(event.id)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
chatterEventSub.onSubscriptionDeleteFailure(event => {
|
||||||
|
logger.err(`Failed to delete chatter EventSub subscription: ${kleur.underline(event.id)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteDuplicateStreamerSubscriptions = setTimeout(async () => {
|
||||||
|
logger.info('Deleting all double streamer subscriptions');
|
||||||
|
await streamerApi.asUser(streamerId, async tempapi => {
|
||||||
|
const subs = await tempapi.eventSub.getSubscriptionsForStatus("enabled");
|
||||||
|
|
||||||
|
const seen = new Map();
|
||||||
|
const duplicates: HelixEventSubSubscription[] = [];
|
||||||
|
|
||||||
|
for (const sub of subs.data) {
|
||||||
|
if (seen.has(sub.type)) {
|
||||||
|
duplicates.push(sub);
|
||||||
|
} else {
|
||||||
|
seen.set(sub.type, sub);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const sub of duplicates) {
|
||||||
|
await tempapi.eventSub.deleteSubscription(sub.id);
|
||||||
|
logger.ok(`Deleted streamer sub: id: ${sub.id}, type: ${sub.type}`);
|
||||||
|
};
|
||||||
|
if (duplicates.length === 0) logger.ok('No duplicate streamer subscriptions found');
|
||||||
|
else logger.ok('Deleted all duplicate streamer EventSub subscriptions');
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
const deleteDuplicateChatterSubscriptions = setTimeout(async () => {
|
||||||
|
logger.info('Deleting all double chatter subscriptions');
|
||||||
|
await chatterApi.asUser(chatterId, async tempapi => {
|
||||||
|
const subs = await tempapi.eventSub.getSubscriptionsForStatus("enabled");
|
||||||
|
|
||||||
|
const seen = new Map();
|
||||||
|
const duplicates: HelixEventSubSubscription[] = [];
|
||||||
|
|
||||||
|
for (const sub of subs.data) {
|
||||||
|
if (seen.has(sub.type)) {
|
||||||
|
duplicates.push(sub);
|
||||||
|
} else {
|
||||||
|
seen.set(sub.type, sub);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const sub of duplicates) {
|
||||||
|
await tempapi.eventSub.deleteSubscription(sub.id);
|
||||||
|
logger.ok(`Deleted chatter sub: id: ${sub.id}, type: ${sub.type}`);
|
||||||
|
};
|
||||||
|
if (duplicates.length === 0) logger.ok('No duplicate chatter subscriptions found');
|
||||||
|
else logger.ok('Deleted all duplicate chatter EventSub subscriptions');
|
||||||
|
});
|
||||||
|
}, 10000);
|
||||||
@@ -1,27 +1,4 @@
|
|||||||
import kleur from "kleur";
|
import { eventSub, chatterEventSub } from "main";
|
||||||
import { eventSub, streamerApi, streamerId } from "main";
|
|
||||||
import logger from "lib/logger";
|
|
||||||
|
|
||||||
eventSub.onRevoke(event => {
|
|
||||||
logger.ok(`Successfully revoked EventSub subscription: ${kleur.underline(event.id)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSub.onSubscriptionCreateSuccess(event => {
|
|
||||||
logger.ok(`Successfully created EventSub subscription: ${kleur.underline(event.id)}`);
|
|
||||||
deleteDuplicateSubscriptions.refresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSub.onSubscriptionCreateFailure(event => {
|
|
||||||
logger.err(`Failed to create EventSub subscription: ${kleur.underline(event.id)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSub.onSubscriptionDeleteSuccess(event => {
|
|
||||||
logger.ok(`Successfully deleted EventSub subscription: ${kleur.underline(event.id)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSub.onSubscriptionDeleteFailure(event => {
|
|
||||||
logger.err(`Failed to delete EventSub subscription: ${kleur.underline(event.id)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
import { readdir } from 'node:fs/promises';
|
import { readdir } from 'node:fs/promises';
|
||||||
const files = await readdir(import.meta.dir);
|
const files = await readdir(import.meta.dir);
|
||||||
@@ -32,30 +9,4 @@ for (const file of files) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
eventSub.start();
|
eventSub.start();
|
||||||
|
chatterEventSub.start();
|
||||||
import { HelixEventSubSubscription } from "@twurple/api";
|
|
||||||
|
|
||||||
const deleteDuplicateSubscriptions = setTimeout(async () => {
|
|
||||||
logger.info('Deleting all double subscriptions');
|
|
||||||
await streamerApi.asUser(streamerId, async tempapi => {
|
|
||||||
const subs = await tempapi.eventSub.getSubscriptionsForStatus("enabled");
|
|
||||||
|
|
||||||
const seen = new Map();
|
|
||||||
const duplicates: HelixEventSubSubscription[] = [];
|
|
||||||
|
|
||||||
for (const sub of subs.data) {
|
|
||||||
if (seen.has(sub.type)) {
|
|
||||||
duplicates.push(sub);
|
|
||||||
} else {
|
|
||||||
seen.set(sub.type, sub);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const sub of duplicates) {
|
|
||||||
await tempapi.eventSub.deleteSubscription(sub.id);
|
|
||||||
logger.ok(`Deleted sub: id: ${sub.id}, type: ${sub.type}`);
|
|
||||||
};
|
|
||||||
if (duplicates.length === 0) logger.ok('No duplicate subscriptions found');
|
|
||||||
else logger.ok('Deleted all duplicate EventSub subscriptions');
|
|
||||||
});
|
|
||||||
}, 5000);
|
|
||||||
|
|||||||
19
src/events/whisper.ts
Normal file
19
src/events/whisper.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { redis } from "bun";
|
||||||
|
import { sendMessage } from "commands";
|
||||||
|
import { buildTimeString } from "lib/dateManager";
|
||||||
|
import { chatterEventSub, chatterApi, chatterId } from "main";
|
||||||
|
|
||||||
|
const WHISPERCOOLDOWN = 60 * 10; // 10 minutes
|
||||||
|
|
||||||
|
chatterEventSub.onUserWhisperMessage(chatterId, async msg => {
|
||||||
|
if (await redis.ttl(`user:${msg.senderUserId}:timeout`) < 0) return;
|
||||||
|
const cooldown = await redis.expiretime(`user:${msg.senderUserId}:whispercooldown`);
|
||||||
|
if (cooldown < 0) {
|
||||||
|
await redis.set(`user:${msg.senderUserId}:whispercooldown`, '1');
|
||||||
|
await redis.expire(`user:${msg.senderUserId}:whispercooldown`, WHISPERCOOLDOWN);
|
||||||
|
await sendMessage(`${msg.senderUserDisplayName} whispered: ${msg.messageText}`);
|
||||||
|
await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, "Message sent. Please wait 10 minutes until you can send another message.");
|
||||||
|
} else {
|
||||||
|
await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, `Wait another ${buildTimeString(cooldown * 1000, Date.now())} before sending another message.`);
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -7,9 +7,11 @@ import { addInvuln } from "lib/invuln";
|
|||||||
import { redis } from "bun";
|
import { redis } from "bun";
|
||||||
import { remodMod, timeoutDuration } from "lib/timeout";
|
import { remodMod, timeoutDuration } from "lib/timeout";
|
||||||
import User from "user";
|
import User from "user";
|
||||||
import { buildTimeString } from "lib/dateManager";
|
import { connectionCheck } from "connectionCheck";
|
||||||
|
|
||||||
const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot"];
|
await connectionCheck()
|
||||||
|
|
||||||
|
const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot", "user:manage:whispers"];
|
||||||
const STREAMERINTENTS = ["channel:bot", "user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:chat_messages", "moderator:manage:banned_users", "bits:read", "channel:moderate", "moderator:manage:shoutouts"];
|
const STREAMERINTENTS = ["channel:bot", "user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:chat_messages", "moderator:manage:banned_users", "bits:read", "channel:moderate", "moderator:manage:shoutouts"];
|
||||||
|
|
||||||
export const singleUserMode = process.env.CHATTER_IS_STREAMER === 'true';
|
export const singleUserMode = process.env.CHATTER_IS_STREAMER === 'true';
|
||||||
@@ -30,6 +32,8 @@ export const streamerApi = streamerAuthProvider ? new ApiClient({ authProvider:
|
|||||||
/** As the streamerApi has either the streamer or the chatter if the chatter IS the streamer this has streamer permissions */
|
/** As the streamerApi has either the streamer or the chatter if the chatter IS the streamer this has streamer permissions */
|
||||||
export const eventSub = new EventSubWsListener({ apiClient: streamerApi });
|
export const eventSub = new EventSubWsListener({ apiClient: streamerApi });
|
||||||
|
|
||||||
|
export const chatterEventSub = singleUserMode ? eventSub : new EventSubWsListener({ apiClient: chatterApi });
|
||||||
|
|
||||||
export const commandPrefix = process.env.COMMAND_PREFIX ?? "!";
|
export const commandPrefix = process.env.COMMAND_PREFIX ?? "!";
|
||||||
|
|
||||||
export const streamerUsers = [chatterId, streamerId];
|
export const streamerUsers = [chatterId, streamerId];
|
||||||
|
|||||||
Reference in New Issue
Block a user