diff --git a/src/commands/definitions/blacklist-mention.ts b/src/commands/definitions/blacklist-mention.ts new file mode 100644 index 0000000..9a6b204 --- /dev/null +++ b/src/commands/definitions/blacklist-mention.ts @@ -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; diff --git a/src/commands/definitions/unblacklist-mention.ts b/src/commands/definitions/unblacklist-mention.ts new file mode 100644 index 0000000..aaf3192 --- /dev/null +++ b/src/commands/definitions/unblacklist-mention.ts @@ -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; diff --git a/src/core/config.ts b/src/core/config.ts index c02377a..f661337 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -131,8 +131,8 @@ export const config: BotConfig = { mentionCooldown: 24 * 60 * 60 * 1000, // 24 hours mentionProbability: 0.001, spontaneousSchedulerEnabled: getBooleanEnvOrDefault("BOT_SPONTANEOUS_SCHEDULER_ENABLED", true), - spontaneousSchedulerMinIntervalMs: parseInt(getEnvOrDefault("BOT_SPONTANEOUS_MIN_INTERVAL_MS", String(45 * 60 * 1000))), - spontaneousSchedulerMaxIntervalMs: parseInt(getEnvOrDefault("BOT_SPONTANEOUS_MAX_INTERVAL_MS", String(180 * 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(7 * 24 * 60 * 60 * 1000))), // 7 days }, web: { port: parseInt(getEnvOrDefault("WEB_PORT", "3000")), diff --git a/src/database/drizzle/0007_peaceful_juggernaut.sql b/src/database/drizzle/0007_peaceful_juggernaut.sql new file mode 100644 index 0000000..027b7d6 --- /dev/null +++ b/src/database/drizzle/0007_peaceful_juggernaut.sql @@ -0,0 +1 @@ +SELECT 1; \ No newline at end of file diff --git a/src/database/repositories/user.repository.ts b/src/database/repositories/user.repository.ts index 4e36918..3d7ade3 100644 --- a/src/database/repositories/user.repository.ts +++ b/src/database/repositories/user.repository.ts @@ -39,6 +39,11 @@ export const userRepository = { }); }, + async getOptedOutUserIds(): Promise { + 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 { await db .insert(membership) diff --git a/src/features/joel/mentions.ts b/src/features/joel/mentions.ts index 365aad6..fe6de75 100644 --- a/src/features/joel/mentions.ts +++ b/src/features/joel/mentions.ts @@ -6,6 +6,7 @@ import type { Message } from "discord.js"; import { config } from "../../core/config"; import { createLogger } from "../../core/logger"; +import { userRepository } from "../../database"; const logger = createLogger("Features:Mentions"); @@ -22,8 +23,9 @@ export async function getRandomMemberMention( ): Promise { try { 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( (member) => !member.user.bot && !excludedSet.has(member.id) ); diff --git a/src/features/joel/spontaneous-cron.ts b/src/features/joel/spontaneous-cron.ts index 3789184..e89c1b9 100644 --- a/src/features/joel/spontaneous-cron.ts +++ b/src/features/joel/spontaneous-cron.ts @@ -7,16 +7,18 @@ import { eq } from "drizzle-orm"; import type { BotClient } from "../../core/client"; import { config } from "../../core/config"; import { createLogger } from "../../core/logger"; -import { db } from "../../database"; +import { db, userRepository } from "../../database"; import { botOptions } from "../../database/schema"; import { getAiService } from "../../services/ai"; const logger = createLogger("Features:Joel:SpontaneousCron"); const SPONTANEOUS_TOPICS = [ - "a weird thought", - "a funny joke", - "a chaotic world update", + "an unhinged shower thought", + "a completely unsolicited aggressive life advice", + "a bizarre threat that makes no sense", + "a weirdly specific and insulting observation", + "a chaotic question that demands an answer", ] as const; let timer: ReturnType | null = null; @@ -151,7 +153,10 @@ function isWritableTextChannel(channel: unknown, client: BotClient): channel is async function getRandomGuildMemberMention(guild: Guild): Promise { try { 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(); if (!randomMember) { @@ -172,12 +177,14 @@ async function generateNonsense(): Promise { const response = await ai.generateResponse( `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: -- Mention nothing about being an AI -- 1-2 sentences, max 220 characters -- Funny or absurd tone -- No lists, no hashtags` +- The message should make the user think "wtf did he just say?" +- Be aggressive, insulting, or bizarrely threatening, but in a humorous, absurd way (not extremely fucked up or genuinely disturbing). +- Use your signature unhinged personality (swearing is fine, but keep it funny). +- 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, " ");