diff options
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClient.ts | 2 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClientUtil.ts | 60 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuild.ts | 194 | ||||
-rw-r--r-- | src/lib/utils/BushConstants.ts | 8 | ||||
-rw-r--r-- | src/listeners/message/quoteCreate.ts | 23 | ||||
-rw-r--r-- | src/listeners/message/quoteEdit.ts | 17 |
6 files changed, 300 insertions, 4 deletions
diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts index 5a911f7..2fb559c 100644 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ b/src/lib/extensions/discord-akairo/BushClient.ts @@ -343,6 +343,8 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re process.exit(2); } + this.setMaxListeners(20); + this.perspective = await google.discoverAPI<any>('https://commentanalyzer.googleapis.com/$discovery/rest?version=v1alpha1'); this.commandHandler.useInhibitorHandler(this.inhibitorHandler); diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index f5099f6..e92abe7 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -22,7 +22,7 @@ import assert from 'assert'; import { exec } from 'child_process'; import deepLock from 'deep-lock'; import { ClientUtil, Util as AkairoUtil } from 'discord-akairo'; -import { APIEmbed, APIMessage, OAuth2Scopes } from 'discord-api-types/v9'; +import { APIEmbed, APIMessage, OAuth2Scopes, Routes } from 'discord-api-types/v9'; import { Constants as DiscordConstants, Embed, @@ -1000,6 +1000,50 @@ export class BushClientUtil extends ClientUtil { return embeds; } + public async resolveMessageLinks(content: string | null): Promise<MessageLinkParts[]> { + const res: MessageLinkParts[] = []; + + if (!content) return res; + + const regex = new RegExp(this.regex.messageLink); + let match: RegExpExecArray | null; + while (((match = regex.exec(content)), match !== null)) { + const input = match.input; + if (!match.groups || !input) continue; + if (input.startsWith('<') && input.endsWith('>')) continue; + + const { guild_id, channel_id, message_id } = match.groups; + if (!guild_id || !channel_id || !message_id) continue; + + res.push({ guild_id, channel_id, message_id }); + } + + return res; + } + + public async resolveMessagesFromLinks(content: string): Promise<APIMessage[]> { + const res: APIMessage[] = []; + + const links = await this.resolveMessageLinks(content); + if (!links.length) return []; + + for (const { guild_id, channel_id, message_id } of links) { + const guild = client.guilds.cache.get(guild_id); + if (!guild) continue; + const channel = guild.channels.cache.get(channel_id); + if (!channel || (!channel.isTextBased() && !channel.isThread())) continue; + + const message = (await client.rest + .get(Routes.channelMessage(channel_id, message_id)) + .catch(() => null)) as APIMessage | null; + if (!message) continue; + + res.push(message); + } + + return res; + } + /** * A wrapper for the Argument class that adds custom typings. */ @@ -1034,6 +1078,14 @@ export class BushClientUtil extends ClientUtil { public get akairo() { return AkairoUtil; } + + public get consts() { + return client.consts; + } + + public get regex() { + return client.consts.regex; + } } interface HastebinRes { @@ -1075,3 +1127,9 @@ export interface ParsedDurationRes { } export type TimestampStyle = 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R'; + +export interface MessageLinkParts { + guild_id: Snowflake; + channel_id: Snowflake; + message_id: Snowflake; +} diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index 7fadd96..b0a34e7 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -1,5 +1,7 @@ import { + AllowedMentions, banResponse, + BushMessage, dmResponse, permissionsResponse, punishmentEntryRemove, @@ -18,7 +20,20 @@ import { type GuildLogType, type GuildModel } from '#lib'; -import { Collection, Guild, PermissionFlagsBits, Snowflake, type MessageOptions, type MessagePayload } from 'discord.js'; +import { APIMessage } from 'discord-api-types/v9'; +import { + Collection, + Guild, + MessageType, + PermissionFlagsBits, + SnowflakeUtil, + ThreadChannel, + Webhook, + WebhookMessageOptions, + type MessageOptions, + type MessagePayload, + type Snowflake +} from 'discord.js'; import type { RawGuildData } from 'discord.js/typings/rawDataTypes'; import _ from 'lodash'; import { Moderation } from '../../common/util/Moderation.js'; @@ -449,6 +464,183 @@ export class BushGuild extends Guild { client.emit(options.unlock ? 'bushUnlockdown' : 'bushLockdown', moderator, options.reason, success, options.all); return ret; } + + public async quote(rawQuote: APIMessage, channel: BushTextChannel | BushNewsChannel | BushThreadChannel) { + if (!channel.isTextBased() || channel.isDMBased() || channel.guildId !== this.id || !this.me) return null; + if (!channel.permissionsFor(this.me).has('ManageWebhooks')) return null; + + const quote = new BushMessage(client, rawQuote); + + const target = channel instanceof ThreadChannel ? channel.parent : channel; + if (!target) return null; + + const webhooks: Collection<string, Webhook> = await target.fetchWebhooks().catch((e) => e); + if (!(webhooks instanceof Collection)) return null; + + // find a webhook that we can use + let webhook = webhooks.find((w) => !!w.token) ?? null; + if (!webhook) + webhook = await target + .createWebhook(`${client.user!.username} Quotes #${target.name}`, { + avatar: client.user!.displayAvatarURL({ size: 2048 }), + reason: 'Creating a webhook for quoting' + }) + .catch(() => null); + + if (!webhook) return null; + + const sendOptions: Omit<WebhookMessageOptions, 'flags'> = {}; + + const displayName = quote.member?.displayName ?? quote.author.username; + + switch (quote.type) { + case MessageType.Default: + case MessageType.Reply: + case MessageType.ChatInputCommand: + case MessageType.ContextMenuCommand: + case MessageType.ThreadStarterMessage: + sendOptions.content = quote.content || undefined; + sendOptions.threadId = channel instanceof ThreadChannel ? channel.id : undefined; + sendOptions.embeds = quote.embeds.length ? quote.embeds : undefined; + sendOptions.attachments = quote.attachments.size ? [...quote.attachments.values()] : undefined; + + if (quote.stickers.size && !(quote.content || quote.embeds.length || quote.attachments.size)) + sendOptions.content = '[[This message has a sticker but not content]]'; + + break; + case MessageType.RecipientAdd: { + const recipient = rawQuote.mentions[0]; + if (!recipient) { + sendOptions.content = `${util.emojis.error} Cannot resolve recipient.`; + break; + } + + if (quote.channel.isThread()) { + const recipientDisplay = quote.guild?.members.cache.get(recipient.id)?.displayName ?? recipient.username; + sendOptions.content = `${util.emojis.join} ${displayName} added ${recipientDisplay} to the thread.`; + } else { + // this should never happen + sendOptions.content = `${util.emojis.join} ${displayName} added ${recipient.username} to the group.`; + } + + break; + } + case MessageType.RecipientRemove: { + const recipient = rawQuote.mentions[0]; + if (!recipient) { + sendOptions.content = `${util.emojis.error} Cannot resolve recipient.`; + break; + } + + if (quote.channel.isThread()) { + const recipientDisplay = quote.guild?.members.cache.get(recipient.id)?.displayName ?? recipient.username; + sendOptions.content = `${util.emojis.leave} ${displayName} removed ${recipientDisplay} from the thread.`; + } else { + // this should never happen + sendOptions.content = `${util.emojis.leave} ${displayName} removed ${recipient.username} from the group.`; + } + + break; + } + + case MessageType.ChannelNameChange: + sendOptions.content = `<:pencil:957988608994861118> ${displayName} changed the channel name: **${quote.content}**`; + + break; + + case MessageType.ChannelPinnedMessage: + throw new Error('Not implemented yet: MessageType.ChannelPinnedMessage case'); + case MessageType.GuildMemberJoin: { + const messages = [ + '{username} joined the party.', + '{username} is here.', + 'Welcome, {username}. We hope you brought pizza.', + 'A wild {username} appeared.', + '{username} just landed.', + '{username} just slid into the server.', + '{username} just showed up!', + 'Welcome {username}. Say hi!', + '{username} hopped into the server.', + 'Everyone welcome {username}!', + "Glad you're here, {username}.", + 'Good to see you, {username}.', + 'Yay you made it, {username}!' + ]; + + const timestamp = SnowflakeUtil.timestampFrom(quote.id); + + // this is the same way that the discord client decides what message to use. + const message = messages[timestamp % messages.length].replace(/{username}/g, displayName); + + sendOptions.content = `${util.emojis.join} ${message}`; + break; + } + case MessageType.UserPremiumGuildSubscription: + sendOptions.content = `<:NitroBoost:585558042309820447> ${displayName} just boosted the server${ + quote.content ? ` **${quote.content}** times` : '' + }!`; + + break; + case MessageType.UserPremiumGuildSubscriptionTier1: + case MessageType.UserPremiumGuildSubscriptionTier2: + case MessageType.UserPremiumGuildSubscriptionTier3: + sendOptions.content = `<:NitroBoost:585558042309820447> ${displayName} just boosted the server${ + quote.content ? ` **${quote.content}** times` : '' + }! ${quote.guild?.name} has achieved **Level ${quote.type - 8}!**`; + + break; + case MessageType.ChannelFollowAdd: + sendOptions.content = `${displayName} has added **${quote.content}** to this channel. Its most important updates will show up here.`; + + break; + case MessageType.GuildDiscoveryDisqualified: + sendOptions.content = + '<:SystemMessageCross:842172192418693173> This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details.'; + + break; + case MessageType.GuildDiscoveryRequalified: + sendOptions.content = + '<:SystemMessageCheck:842172191801212949> This server is eligible for Server Discovery again and has been automatically relisted!'; + + break; + case MessageType.GuildDiscoveryGracePeriodInitialWarning: + sendOptions.content = + '<:SystemMessageWarn:842172192401915971> This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery.'; + + break; + case MessageType.GuildDiscoveryGracePeriodFinalWarning: + sendOptions.content = + '<:SystemMessageWarn:842172192401915971> This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery.'; + + break; + case MessageType.ThreadCreated: { + const threadId = rawQuote.message_reference?.channel_id; + + sendOptions.content = `<:thread:865033845753249813> ${displayName} started a thread: **[${quote.content}](https://discord.com/channels/${quote.guildId}/${threadId} + )**. See all threads.`; + + break; + } + case MessageType.GuildInviteReminder: + sendOptions.content = 'Wondering who to invite? Start by inviting anyone who can help you build the server!'; + + break; + case MessageType.ChannelIconChange: + case MessageType.Call: + default: + sendOptions.content = `${util.emojis.error} I cannot quote **${ + MessageType[quote.type] || quote.type + }** messages, please report this to my developers.`; + + break; + } + + sendOptions.allowedMentions = AllowedMentions.none(); + sendOptions.username = quote.member?.displayName ?? quote.author.username; + sendOptions.avatarURL = quote.member?.displayAvatarURL({ size: 2048 }) ?? quote.author.displayAvatarURL({ size: 2048 }); + + return await webhook.send(sendOptions); /* .catch((e: any) => e); */ + } } export interface BushGuild extends Guild { diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts index 8d35522..df69806 100644 --- a/src/lib/utils/BushConstants.ts +++ b/src/lib/utils/BushConstants.ts @@ -154,9 +154,13 @@ export class BushConstants { discordEmoji: /<a?:(?<name>[a-zA-Z0-9_]+):(?<id>\d{15,21})>/im, - //stolen from geek + /* + * Taken with permission from Geek: + * https://github.com/FireDiscordBot/bot/blob/5d1990e5f8b52fcc72261d786aa3c7c7c65ab5e8/lib/util/constants.ts#L276 + */ + /** **This has the global flag, make sure to handle it correctly.** */ messageLink: - /(?:ptb\.|canary\.|staging\.|lc\.)?(?:discord(?:app)?)\.(?:com)?\/channels\/(?<guild_id>\d{15,21}|@me)\/(?<channel_id>\d{15,21})\/(?<message_id>\d{15,21})/im + /<?(?:ptb\.|canary\.|staging\.)?discord(?:app)?\.com?\/channels\/(?<guild_id>\d{15,21})\/(?<channel_id>\d{15,21})\/(?<message_id>\d{15,21})>?/gim } as const); /** diff --git a/src/listeners/message/quoteCreate.ts b/src/listeners/message/quoteCreate.ts new file mode 100644 index 0000000..08fd0cf --- /dev/null +++ b/src/listeners/message/quoteCreate.ts @@ -0,0 +1,23 @@ +import { BushListener, type BushClientEvents } from '#lib'; + +export default class QuoteCreateListener extends BushListener { + public constructor() { + super('quoteCreate', { + emitter: 'client', + event: 'messageCreate', + category: 'message' + }); + } + + public override async exec(...[message]: BushClientEvents['messageCreate']) { + if (message.author.id !== '322862723090219008') return; + if (!message.inGuild()) return; + + const messages = await util.resolveMessagesFromLinks(message.content); + if (!messages.length) return; + + for (const msg of messages) { + await message.guild.quote(msg, message.channel); + } + } +} diff --git a/src/listeners/message/quoteEdit.ts b/src/listeners/message/quoteEdit.ts new file mode 100644 index 0000000..790f05a --- /dev/null +++ b/src/listeners/message/quoteEdit.ts @@ -0,0 +1,17 @@ +// import { BushListener, type BushClientEvents } from '#lib'; + +// export default class QuoteEditListener extends BushListener { +// public constructor() { +// super('quoteEdit', { +// emitter: 'client', +// event: 'messageUpdate', +// category: 'message' +// }); +// } + +// public override async exec(...[_, newMessage]: BushClientEvents['messageUpdate']) { +// return; +// // if (newMessage.partial) newMessage = await newMessage.fetch(); +// // return new QuoteCreateListener().exec(newMessage); +// } +// } |