aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/lib/extensions/discord-akairo/BushClient.ts2
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts60
-rw-r--r--src/lib/extensions/discord.js/BushGuild.ts194
-rw-r--r--src/lib/utils/BushConstants.ts8
-rw-r--r--src/listeners/message/quoteCreate.ts23
-rw-r--r--src/listeners/message/quoteEdit.ts17
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);
+// }
+// }