feat: added option to make joel shut the fuck up

This commit is contained in:
eric
2026-02-25 18:30:29 +01:00
parent 70e8a67113
commit 94ad2896cc
7 changed files with 122 additions and 13 deletions

View File

@@ -0,0 +1,47 @@
/**
* Blacklist mention command
*/
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
import type { Command } from "../types";
import { userRepository } from "../../database";
const command: Command = {
data: new SlashCommandBuilder()
.setName("blacklist-mention")
.setDescription("Blacklist a user (or yourself) from being randomly mentioned by Joel")
.setDMPermission(false)
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to blacklist (requires Manage Guild permission if not yourself)")
.setRequired(false)
),
category: "utility",
execute: async (interaction) => {
const targetUser = interaction.options.getUser("user") || interaction.user;
// If trying to blacklist someone else, check permissions
if (targetUser.id !== interaction.user.id) {
const member = await interaction.guild?.members.fetch(interaction.user.id);
if (!member?.permissions.has(PermissionFlagsBits.ManageGuild)) {
await interaction.reply({
content: "You need the Manage Guild permission to blacklist other users.",
ephemeral: true,
});
return;
}
}
await userRepository.setOptOut(targetUser.id, true);
await interaction.reply({
content: targetUser.id === interaction.user.id
? "You have been successfully blacklisted from random mentions."
: `Successfully blacklisted <@${targetUser.id}> from random mentions.`,
ephemeral: true,
});
},
};
export default command;

View File

@@ -0,0 +1,47 @@
/**
* Unblacklist mention command
*/
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
import type { Command } from "../types";
import { userRepository } from "../../database";
const command: Command = {
data: new SlashCommandBuilder()
.setName("unblacklist-mention")
.setDescription("Remove a user (or yourself) from the random mention blacklist")
.setDMPermission(false)
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to unblacklist (requires Manage Guild permission if not yourself)")
.setRequired(false)
),
category: "utility",
execute: async (interaction) => {
const targetUser = interaction.options.getUser("user") || interaction.user;
// If trying to unblacklist someone else, check permissions
if (targetUser.id !== interaction.user.id) {
const member = await interaction.guild?.members.fetch(interaction.user.id);
if (!member?.permissions.has(PermissionFlagsBits.ManageGuild)) {
await interaction.reply({
content: "You need the Manage Guild permission to unblacklist other users.",
ephemeral: true,
});
return;
}
}
await userRepository.setOptOut(targetUser.id, false);
await interaction.reply({
content: targetUser.id === interaction.user.id
? "You have been successfully removed from the random mention blacklist."
: `Successfully removed <@${targetUser.id}> from the random mention blacklist.`,
ephemeral: true,
});
},
};
export default command;

View File

@@ -131,8 +131,8 @@ export const config: BotConfig = {
mentionCooldown: 24 * 60 * 60 * 1000, // 24 hours mentionCooldown: 24 * 60 * 60 * 1000, // 24 hours
mentionProbability: 0.001, mentionProbability: 0.001,
spontaneousSchedulerEnabled: getBooleanEnvOrDefault("BOT_SPONTANEOUS_SCHEDULER_ENABLED", true), spontaneousSchedulerEnabled: getBooleanEnvOrDefault("BOT_SPONTANEOUS_SCHEDULER_ENABLED", true),
spontaneousSchedulerMinIntervalMs: parseInt(getEnvOrDefault("BOT_SPONTANEOUS_MIN_INTERVAL_MS", String(45 * 60 * 1000))), spontaneousSchedulerMinIntervalMs: parseInt(getEnvOrDefault("BOT_SPONTANEOUS_MIN_INTERVAL_MS", String(2 * 24 * 60 * 60 * 1000))), // 2 days
spontaneousSchedulerMaxIntervalMs: parseInt(getEnvOrDefault("BOT_SPONTANEOUS_MAX_INTERVAL_MS", String(180 * 60 * 1000))), spontaneousSchedulerMaxIntervalMs: parseInt(getEnvOrDefault("BOT_SPONTANEOUS_MAX_INTERVAL_MS", String(7 * 24 * 60 * 60 * 1000))), // 7 days
}, },
web: { web: {
port: parseInt(getEnvOrDefault("WEB_PORT", "3000")), port: parseInt(getEnvOrDefault("WEB_PORT", "3000")),

View File

@@ -0,0 +1 @@
SELECT 1;

View File

@@ -39,6 +39,11 @@ export const userRepository = {
}); });
}, },
async getOptedOutUserIds(): Promise<string[]> {
const results = await db.select({ id: users.id }).from(users).where(eq(users.opt_out, 1));
return results.map((r) => r.id);
},
async addMembership(userId: string, guildId: string): Promise<void> { async addMembership(userId: string, guildId: string): Promise<void> {
await db await db
.insert(membership) .insert(membership)

View File

@@ -6,6 +6,7 @@
import type { Message } from "discord.js"; import type { Message } from "discord.js";
import { config } from "../../core/config"; import { config } from "../../core/config";
import { createLogger } from "../../core/logger"; import { createLogger } from "../../core/logger";
import { userRepository } from "../../database";
const logger = createLogger("Features:Mentions"); const logger = createLogger("Features:Mentions");
@@ -22,8 +23,9 @@ export async function getRandomMemberMention(
): Promise<string | null> { ): Promise<string | null> {
try { try {
const members = await message.guild.members.fetch({ limit: 100 }); const members = await message.guild.members.fetch({ limit: 100 });
const optedOutUsers = await userRepository.getOptedOutUserIds();
const excludedSet = new Set(excludedUserIds); const excludedSet = new Set([...excludedUserIds, ...optedOutUsers]);
const validMembers = members.filter( const validMembers = members.filter(
(member) => !member.user.bot && !excludedSet.has(member.id) (member) => !member.user.bot && !excludedSet.has(member.id)
); );

View File

@@ -7,16 +7,18 @@ import { eq } from "drizzle-orm";
import type { BotClient } from "../../core/client"; import type { BotClient } from "../../core/client";
import { config } from "../../core/config"; import { config } from "../../core/config";
import { createLogger } from "../../core/logger"; import { createLogger } from "../../core/logger";
import { db } from "../../database"; import { db, userRepository } from "../../database";
import { botOptions } from "../../database/schema"; import { botOptions } from "../../database/schema";
import { getAiService } from "../../services/ai"; import { getAiService } from "../../services/ai";
const logger = createLogger("Features:Joel:SpontaneousCron"); const logger = createLogger("Features:Joel:SpontaneousCron");
const SPONTANEOUS_TOPICS = [ const SPONTANEOUS_TOPICS = [
"a weird thought", "an unhinged shower thought",
"a funny joke", "a completely unsolicited aggressive life advice",
"a chaotic world update", "a bizarre threat that makes no sense",
"a weirdly specific and insulting observation",
"a chaotic question that demands an answer",
] as const; ] as const;
let timer: ReturnType<typeof setTimeout> | null = null; let timer: ReturnType<typeof setTimeout> | null = null;
@@ -151,7 +153,10 @@ function isWritableTextChannel(channel: unknown, client: BotClient): channel is
async function getRandomGuildMemberMention(guild: Guild): Promise<string | null> { async function getRandomGuildMemberMention(guild: Guild): Promise<string | null> {
try { try {
const members = await guild.members.fetch({ limit: 200 }); const members = await guild.members.fetch({ limit: 200 });
const eligible = members.filter((member) => !member.user.bot); const optedOutUsers = await userRepository.getOptedOutUserIds();
const excludedSet = new Set(optedOutUsers);
const eligible = members.filter((member) => !member.user.bot && !excludedSet.has(member.id));
const randomMember = eligible.random(); const randomMember = eligible.random();
if (!randomMember) { if (!randomMember) {
@@ -172,12 +177,14 @@ async function generateNonsense(): Promise<string | null> {
const response = await ai.generateResponse( const response = await ai.generateResponse(
`Write one short Discord message containing ${topic}.`, `Write one short Discord message containing ${topic}.`,
`You are Joel posting an unsolicited random message. `You are Joel, an unhinged, aggressive, and chaotic Discord bot. You are randomly pinging a user to say something completely unsolicited.
Rules: Rules:
- Mention nothing about being an AI - The message should make the user think "wtf did he just say?"
- 1-2 sentences, max 220 characters - Be aggressive, insulting, or bizarrely threatening, but in a humorous, absurd way (not extremely fucked up or genuinely disturbing).
- Funny or absurd tone - Use your signature unhinged personality (swearing is fine, but keep it funny).
- No lists, no hashtags` - Mention nothing about being an AI.
- 1-2 sentences, max 220 characters.
- No lists, no hashtags, no greetings.`
); );
const text = response.text.trim().replace(/\s+/g, " "); const text = response.text.trim().replace(/\s+/g, " ");