diff --git a/app/services/Starboard.ts b/app/services/Starboard.ts index 2659ba5..53f859a 100644 --- a/app/services/Starboard.ts +++ b/app/services/Starboard.ts @@ -5,8 +5,11 @@ import Discord from "discord.js"; import config from "@/config/starboard.json"; import discordConfig from "@/config/discord.json"; -const DEFAULT_AMOUNT = config.amount; -const DEFAULT_EMOTE = config.defaultEmote; +const STARBOARD_CONFIG = { + MESSAGE_AGE_LIMIT_MS: 3 * 28 * 24 * 60 * 60 * 1000, // 3 months + DEFAULT_AMOUNT: config.amount, + DEFAULT_EMOTE: config.defaultEmote, +} as const; export class Starboard extends Service { name = "Starboard"; @@ -57,154 +60,163 @@ export class Starboard extends Service { } public async handleReactionAdded(reaction: Discord.MessageReaction): Promise { - const client = reaction.client; - const channel = reaction.message.channel as Discord.GuildChannel; - const parent = channel.parentId; - - if (config.channelIgnores.includes(channel.id)) return; - if (parent && config.categoryIgnores.includes(parent)) return; - - let needed: number = DEFAULT_AMOUNT; - let emojiFilter: string[] | undefined = [DEFAULT_EMOTE]; - let targetChannel: Discord.Channel | undefined = client.channels.cache.get( - discordConfig.channels.h - ); - let title: string | undefined; - let shouldReact = false; - - if (reaction.emoji.name !== DEFAULT_EMOTE) { - switch (parent) { - // the parent of a thread is the main channel, so we sadly can't get the category without fetching, so much for dry - case discordConfig.channels.postYourStuff: - emojiFilter = undefined; - shouldReact = true; - needed = 6; - title = - reaction.message.channel.isThread() && - reaction.message.id === reaction.message.channel.id - ? reaction.message.channel.name - : undefined; - targetChannel = client.channels.cache.get(discordConfig.channels.hArt); - break; - default: - switch (channel.id) { - case discordConfig.channels.artChat: - emojiFilter = undefined; - shouldReact = true; - needed = 6; - targetChannel = client.channels.cache.get(discordConfig.channels.hArt); - break; - } - } - } + if (this.isBusy) return; - const ego = reaction.message.author - ? reaction.users.cache.has(reaction.message.author.id) - : false; - const count = ego ? reaction.count - 1 : reaction.count; - - if ( - count >= needed && - !this.isBusy && - (emojiFilter ? emojiFilter.includes(reaction.emoji.name ?? "") : true) - ) { - this.isBusy = true; - const msg = await reaction.message.fetch(); - if (!msg) { - console.error("[Starboard] couldn't fetch message", reaction); - this.isBusy = false; - return; - } + try { + const client = reaction.client; + const channel = reaction.message.channel as Discord.GuildChannel; + const parent = channel.parentId; - if (msg.author.bot) - targetChannel = client.channels.cache.get(discordConfig.channels.hBot); + if (config.channelIgnores.includes(channel.id)) return; + if (parent && config.categoryIgnores.includes(parent)) return; - if (!targetChannel) { - console.error("[Starboard] wtf invalid channel", reaction); - this.isBusy = false; - return; + let needed: number = STARBOARD_CONFIG.DEFAULT_AMOUNT; + let emojiFilter: string[] | undefined = [STARBOARD_CONFIG.DEFAULT_EMOTE]; + let targetChannel: Discord.Channel | undefined = client.channels.cache.get( + discordConfig.channels.h + ); + let title: string | undefined; + let shouldReact = false; + + if (reaction.emoji.name !== STARBOARD_CONFIG.DEFAULT_EMOTE) { + switch (parent) { + // the parent of a thread is the main channel, so we sadly can't get the category without fetching, so much for dry + case discordConfig.channels.postYourStuff: + emojiFilter = undefined; + shouldReact = true; + needed = 6; + title = + reaction.message.channel.isThread() && + reaction.message.id === reaction.message.channel.id + ? reaction.message.channel.name + : undefined; + targetChannel = client.channels.cache.get(discordConfig.channels.hArt); + break; + default: + switch (channel.id) { + case discordConfig.channels.artChat: + emojiFilter = undefined; + shouldReact = true; + needed = 6; + targetChannel = client.channels.cache.get( + discordConfig.channels.hArt + ); + break; + } + } } - // check against our local db first - if (await this.isMsgStarred(msg.id)) { - this.isBusy = false; - return; - } + const ego = reaction.message.author + ? reaction.users.cache.has(reaction.message.author.id) + : false; + const count = ego ? reaction.count - 1 : reaction.count; + + if ( + count >= needed && + (emojiFilter ? emojiFilter.includes(reaction.emoji.name ?? "") : true) + ) { + this.isBusy = true; + const msg = await reaction.message.fetch(); + if (!msg) { + console.error("[Starboard] couldn't fetch message", reaction); + this.isBusy = false; + return; + } - // skip messages older than 3 months - if (Date.now() - msg.createdTimestamp > 3 * 28 * 24 * 60 * 60 * 1000) { - this.isBusy = false; - return; - } + if (msg.author.bot) + targetChannel = client.channels.cache.get(discordConfig.channels.hBot); - let text = title ? `## ${title}\n` : ""; - - const reference = msg.reference; - if (reference && reference.messageId) { - const refMsg = await ( - client.channels.cache.get(reference.channelId) as Discord.TextChannel - ).messages.fetch(reference.messageId); - - text += `${ - reference - ? `[replying to ${ - refMsg.system ? "System Message" : refMsg.author.username - }](${refMsg.url})\n` - : "" - }`; - } + if (!targetChannel) { + console.error("[Starboard] wtf invalid channel", reaction); + this.isBusy = false; + return; + } - text += msg.content; - text += msg.stickers.size > 0 ? msg.stickers.first()?.url : ""; + // check against our local db first + if (await this.isMsgStarred(msg.id)) { + this.isBusy = false; + return; + } - const files: string[] = []; - msg.attachments.map(a => files.push(a.url)); + // skip messages older than 3 months + if (Date.now() - msg.createdTimestamp > STARBOARD_CONFIG.MESSAGE_AGE_LIMIT_MS) { + this.isBusy = false; + return; + } - const channel = targetChannel as Discord.TextChannel; + let text = title ? `## ${title}\n` : ""; + + const reference = msg.reference; + if (reference && reference.messageId) { + const refMsg = await ( + client.channels.cache.get(reference.channelId) as Discord.TextChannel + ).messages.fetch(reference.messageId); + + text += `${ + reference + ? `[replying to ${ + refMsg.system ? "System Message" : refMsg.author.username + }](${refMsg.url})\n` + : "" + }`; + } - // we need a webhook created by the application so we can attach components - const webhooks = await channel.fetchWebhooks(); - let webhook = webhooks.find( - h => h.applicationId === discordConfig.bot.applicationId && h.token - ); - if (!webhook) webhook = await channel.createWebhook({ name: "metaconcord starboard" }); - - if (webhook) { - const starred = await webhook - .send({ - content: text, - avatarURL: msg.author.avatarURL() ?? "", - username: msg.author.username, - allowedMentions: { parse: ["users", "roles"] }, - files: files, - embeds: msg.author.bot ? msg.embeds : undefined, - components: [ - { - type: Discord.ComponentType.ActionRow, - components: [ - { - type: Discord.ComponentType.Button, - label: "Jump to message", - style: Discord.ButtonStyle.Link, - url: msg.url, - }, - { - type: Discord.ComponentType.Button, - label: "Delete", - style: Discord.ButtonStyle.Danger, - customId: `starboard:${msg.id}:${msg.channelId}`, - }, - ], - }, - ], - }) - .catch(); - if (starred) { - await this.starMsg(msg.id); - if (shouldReact) await starred.react(reaction.emoji); + text += msg.content; + text += msg.stickers.size > 0 ? msg.stickers.first()?.url : ""; + + const files: string[] = []; + msg.attachments.map(a => files.push(a.url)); + + const channel = targetChannel as Discord.TextChannel; + + // we need a webhook created by the application so we can attach components + const webhooks = await channel.fetchWebhooks(); + let webhook = webhooks.find( + h => h.applicationId === discordConfig.bot.applicationId && h.token + ); + if (!webhook) + webhook = await channel.createWebhook({ name: "metaconcord starboard" }); + + if (webhook) { + const components: Discord.ActionRowBuilder[] = [ + new Discord.ActionRowBuilder().addComponents( + new Discord.ButtonBuilder() + .setLabel("Jump to message") + .setStyle(Discord.ButtonStyle.Link) + .setURL(msg.url), + ...(!msg.author.bot + ? [ + new Discord.ButtonBuilder() + .setLabel("Delete") + .setStyle(Discord.ButtonStyle.Danger) + .setCustomId(`starboard:${msg.id}:${msg.channelId}`), + ] + : []) + ), + ]; + + const starred = await webhook + .send({ + content: text, + avatarURL: msg.author.avatarURL() ?? "", + username: msg.author.username, + allowedMentions: { parse: ["users", "roles"] }, + files: files, + embeds: msg.author.bot ? msg.embeds : undefined, + components, + }) + .catch(); + if (starred) { + await this.starMsg(msg.id); + if (shouldReact) await starred.react(reaction.emoji); + } } - } + this.isBusy = false; + } + } catch (error) { + console.error("[Starboard] Error handling reaction:", error); + } finally { this.isBusy = false; } } diff --git a/app/services/discord/modules/commands/RemoveHighlightMessage.ts b/app/services/discord/modules/commands/RemoveHighlightMessage.ts index 6ed0d77..e58eb7f 100644 --- a/app/services/discord/modules/commands/RemoveHighlightMessage.ts +++ b/app/services/discord/modules/commands/RemoveHighlightMessage.ts @@ -5,59 +5,73 @@ import discordConfig from "@/config/discord.json"; export const MenuRemoveHighlightMessageCommand: MenuCommand = { options: { - name: "remove highlighted message", + name: "remove message from highlights", type: Discord.ApplicationCommandType.Message, }, execute: async (ctx: Discord.MessageContextMenuCommandInteraction, bot) => { - const starred = bot.container.getService("Starboard")?.isMsgStarred(ctx.targetId); - if (!starred) { - await ctx.reply( - EphemeralResponse( - "this command only work on messages that have been posted to the highlight channels..." - ) - ); - return; - } + try { + if (ctx.targetMessage.author.username !== ctx.user.username) { + await ctx.reply(EphemeralResponse("you can only delete your own messages...")); + return; + } - if (ctx.targetMessage.author.username !== ctx.user.username) { - await ctx.reply(EphemeralResponse("you can only delete your own messages...")); - return; - } + const starboardService = bot.container.getService("Starboard"); + if (!starboardService) { + throw new Error("Starboard service not found"); + } - // get the channel the original message was posted in and what highlight channel it was posted to - const channel = (await ctx.channel?.fetch()) as Discord.TextChannel; - let targetChannel: Discord.TextChannel | undefined; - switch (channel.parentId) { - case discordConfig.channels.postYourStuff: - targetChannel = bot.getTextChannel(discordConfig.channels.hArt); - break; - default: - if (channel.id === discordConfig.channels.artChat) { + const starred = await starboardService.isMsgStarred(ctx.targetId); + if (!starred) { + await ctx.reply( + EphemeralResponse( + `This command only works on messages that have been posted to the highlight channels (<#${discordConfig.channels.h}>, <#${discordConfig.channels.hArt}>).` + ) + ); + return; + } + + // get the channel the original message was posted in and what highlight channel it was posted to + const channel = (await ctx.channel?.fetch()) as Discord.TextChannel; + let targetChannel: Discord.TextChannel | undefined; + switch (channel.parentId) { + case discordConfig.channels.postYourStuff: targetChannel = bot.getTextChannel(discordConfig.channels.hArt); break; - } - targetChannel = bot.getTextChannel(discordConfig.channels.h); - break; - } - - // get the message from the highlight channel and delete it - const messages = await targetChannel?.messages.fetch(); - const targetMessage = messages?.find( - // this will not get messages that have been edited in post like replies - m => - m.author.username === ctx.user.username && - m.content === ctx.targetMessage.content && - m.attachments.size > 0 && - m.attachments.first()?.name === ctx.targetMessage.attachments.first()?.name - ); - const deleted = await targetMessage?.delete().catch(console.error); + default: + if (channel.id === discordConfig.channels.artChat) { + targetChannel = bot.getTextChannel(discordConfig.channels.hArt); + break; + } + targetChannel = bot.getTextChannel(discordConfig.channels.h); + break; + } - if (deleted) { - await ctx.reply(EphemeralResponse("👍")); - bot.getTextChannel(bot.config.channels.log)?.send( - `Highlighted Message ${ctx.targetMessage} (${ctx.targetId}) in ${ctx.channel} deleted by ${ctx.user} (${ctx.user.id})` + // get the message from the highlight channel and delete it + const messages = await targetChannel?.messages.fetch(); + const targetMessage = messages?.find( + // this will not get messages that have been edited in post like replies + m => + m.author.username === ctx.user.username && + m.content === ctx.targetMessage.content && + m.attachments.size > 0 && + m.attachments.first()?.name === ctx.targetMessage.attachments.first()?.name ); - } else { + const deleted = await targetMessage?.delete().catch(console.error); + + if (deleted) { + await ctx.reply(EphemeralResponse("👍")); + bot.getTextChannel(bot.config.channels.log)?.send( + `Highlighted Message ${ctx.targetMessage} (${ctx.targetId}) in ${ctx.channel} deleted by ${ctx.user} (${ctx.user.id})` + ); + } else { + await ctx.reply( + EphemeralResponse( + "something went wrong with deleting your message :( ping @techbot if you want it removed" + ) + ); + } + } catch (error) { + console.error("[RemoveHighlightMessage]", error); await ctx.reply( EphemeralResponse( "something went wrong with deleting your message :( ping @techbot if you want it removed" diff --git a/app/services/discord/modules/shitposting.ts b/app/services/discord/modules/shitposting.ts index 3dc7aa1..4f61727 100644 --- a/app/services/discord/modules/shitposting.ts +++ b/app/services/discord/modules/shitposting.ts @@ -36,19 +36,16 @@ const STICKER_FREQ = 0.02; // how often to reply with just a sticker const REPLY_FREQ = 0.5; // how often to when to take a word from a previous message if provided const ALLOWED_IMG_PROVIDERS = ["tenor", "imgur", "discordapp", "tumblr"]; +const MEDIA_URL_REGEX = new RegExp( + `https?://(?:(?:\\w+)?\\.?)+(?:${ALLOWED_IMG_PROVIDERS.join("|")})\\.(?:com|io)/[^?\\s]+` +); const getMediaUrl = (url: string) => { - const match = url.match( - new RegExp( - `https?://(?:(?:\\w+)?\\.?)+(?:${ALLOWED_IMG_PROVIDERS.join( - "|" - )})\\.(?:com|io)/[^?\\s]+` - ) - ); + const match = url.match(MEDIA_URL_REGEX); if (match) return match[0]; }; -const IGNORE_LIST = ["437294613976449024"]; +const IGNORE_LIST = new Set(["437294613976449024"]); function getWord(msg?: string) { if (!msg) return undefined; @@ -397,7 +394,7 @@ export default async (bot: DiscordBot) => { const id = bot.discord.user?.id; if (!id) return; - if (IGNORE_LIST.includes(msg.author.id)) return; + if (IGNORE_LIST.has(msg.author.id)) return; if (msg.partial) { try {