From 7615c003286c1264a31a059df76eb61b774594f7 Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Fri, 2 Dec 2022 11:47:09 +0530 Subject: [PATCH 01/14] routes(status): update response structure Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/routes/status.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/status.ts b/src/routes/status.ts index 0821461d8..42ffc652d 100644 --- a/src/routes/status.ts +++ b/src/routes/status.ts @@ -15,7 +15,7 @@ router.get("/", auth, async (req: Request, res: Response, next: NextFunction): P const shardingManger: ShardingManager = req.app.get("shard-manager"); const shards = await shardingManger.broadcastEval(client => ({ - shard: client.shard.ids.join(" / "), + id: client.shard.ids.join(" / "), uptime: client.uptime, wsStatus: client.ws.status, wsPing: client.ws.ping, From 713731ea635a193c3a941dd6e4fca0d6a5bafa3a Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Fri, 2 Dec 2022 11:48:17 +0530 Subject: [PATCH 02/14] commands(channel#create): don't set parent when creating categories Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/commands/channel/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/channel/create.ts b/src/commands/channel/create.ts index b24ce0994..e4178b9fb 100644 --- a/src/commands/channel/create.ts +++ b/src/commands/channel/create.ts @@ -83,7 +83,7 @@ class ChannelCreateCommand extends Command { bitrate: interaction.guild.premiumTier ? interaction.guild.premiumTier * 128e3 : 96e3, userLimit: limit, rateLimitPerUser: slowmode, - parent: interaction.channel.parentId, + parent: type === ChannelType.GuildCategory ? undefined :interaction.channel.parentId, reason, }); From 88901f284f17b0ca4a75b50e50d8899c270d7eee Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Fri, 2 Dec 2022 11:49:29 +0530 Subject: [PATCH 03/14] commands(channel#create): GuildNews -> GuildAnnouncement Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/commands/channel/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/channel/create.ts b/src/commands/channel/create.ts index e4178b9fb..8fd449be3 100644 --- a/src/commands/channel/create.ts +++ b/src/commands/channel/create.ts @@ -24,7 +24,7 @@ class ChannelCreateCommand extends Command { choices: [ { name: "Text", value: ChannelType.GuildText }, { name: "Voice", value: ChannelType.GuildVoice }, - { name: "Announcement", value: ChannelType.GuildNews }, + { name: "Announcement", value: ChannelType.GuildAnnouncement }, { name: "Stage", value: ChannelType.GuildStageVoice }, { name: "Category", value: ChannelType.GuildCategory }, ], From 43a7a9262cfa1ef80f1822ca6c0b19d0c125a191 Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Wed, 7 Dec 2022 20:24:46 +0530 Subject: [PATCH 04/14] models(Guild): add starboardThreshold field Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/models/Guild.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/models/Guild.ts b/src/models/Guild.ts index 69e10a75f..b6dba6a3c 100644 --- a/src/models/Guild.ts +++ b/src/models/Guild.ts @@ -45,12 +45,14 @@ export interface Guild { messageFilterPatterns?: string[]; // spam filters mentionSpamThreshold?: number; + // starboard + starboardChannel?: string; + starboardThreshold?: number; // logs moderationLogChannel?: string; serverLogChannel?: string; // special channels and roles suggestionsChannel?: string; - starboardChannel?: string; reportsChannel?: string; streamerRole?: string; votingChannels?: string[]; @@ -166,22 +168,25 @@ export default mongoose.model("Guild", new mongoose.S mentionSpamThreshold: { type: Number, }, - moderationLogChannel: { + starboardChannel: { type: String, unique: true, sparse: true, }, - serverLogChannel: { + starboardThreshold: { + type: Number, + }, + moderationLogChannel: { type: String, unique: true, sparse: true, }, - suggestionsChannel: { + serverLogChannel: { type: String, unique: true, sparse: true, }, - starboardChannel: { + suggestionsChannel: { type: String, unique: true, sparse: true, From 46beb3b4450ea726119ac28e1ffac6a86822d4e0 Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Wed, 7 Dec 2022 20:26:21 +0530 Subject: [PATCH 05/14] commands(starboard): support for configuring threshold Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/commands/config/starboard.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/commands/config/starboard.ts b/src/commands/config/starboard.ts index 70492f089..00e553a2b 100644 --- a/src/commands/config/starboard.ts +++ b/src/commands/config/starboard.ts @@ -19,6 +19,12 @@ class StarboardCommand extends Command { description: "The channel where starred messages should be sent.", channel_types: [ ChannelType.GuildText ], }, + { + type: ApplicationCommandOptionType.Integer, + name: "threshold", + description: "The minimum number of stars a message needs.", + min_value: 2, + }, ], userPermissions: [ PermissionFlagsBits.ManageGuild ], }); @@ -27,11 +33,13 @@ class StarboardCommand extends Command { public async exec(interaction: ChatInputCommandInteraction<"cached">): Promise { await interaction.deferReply(); const channel = interaction.options.getChannel("channel"); + const threshold = interaction.options.getInteger("threshold"); const guildDocument = await GuildModel.findById(interaction.guildId); // update starboard channel guildDocument.starboardChannel = channel?.id || undefined; + guildDocument.starboardThreshold = threshold || undefined; await guildDocument.save(); return await interaction.editReply(`I've ${ channel?.id ? "enabled" : "disabled" } starboard${ channel?.id ? ` in the **${ channel.name }** channel` : "" }.`); From 9dbecb56b031df488e69a4612ac75dbf7c5f01c4 Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Thu, 8 Dec 2022 20:05:15 +0530 Subject: [PATCH 06/14] bastion: listen to message reaction events Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/bastion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bastion.ts b/src/bastion.ts index 0b6f59d5f..95e4d683b 100644 --- a/src/bastion.ts +++ b/src/bastion.ts @@ -21,7 +21,7 @@ const bastion = new Client({ GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildMessages, - // GatewayIntentBits.GuildMessageReactions, + GatewayIntentBits.GuildMessageReactions, // GatewayIntentBits.GuildMessageTyping, GatewayIntentBits.DirectMessages, // GatewayIntentBits.DirectMessageReactions, From b91e798796329ba4ad670a0bc4aa6c25ecdb97d1 Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Thu, 8 Dec 2022 20:05:55 +0530 Subject: [PATCH 07/14] listeners: add messageReactionAdd listener Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/listeners/messageReactionAdd.ts | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/listeners/messageReactionAdd.ts diff --git a/src/listeners/messageReactionAdd.ts b/src/listeners/messageReactionAdd.ts new file mode 100644 index 000000000..51ec5dd8c --- /dev/null +++ b/src/listeners/messageReactionAdd.ts @@ -0,0 +1,83 @@ +/*! + * @author TRACTION (iamtraction) + * @copyright 2022 + */ +import { GuildTextBasedChannel, MessageReaction, PartialMessageReaction, PartialUser, Snowflake, User } from "discord.js"; +import { Listener } from "@bastion/tesseract"; + +import GuildModel from "../models/Guild"; +import memcache from "../utils/memcache"; +import { COLORS } from "../utils/constants"; + +class MessageReactionAddListener extends Listener<"messageReactionAdd"> { + constructor() { + super("messageReactionAdd"); + } + + public async exec(reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser): Promise { + // check whether the reaction was of a star + if (reaction.emoji.name !== "⭐") return; + // check whether the message has the minimum reaction count + if (reaction.count < 2) return; + + // check whether the message is already in starboard + const starboardCache = memcache.get("starboard") as Map || new Map(); + const guildStarboardCache = starboardCache.get(reaction.message.guildId); + if (guildStarboardCache?.includes(reaction.message.id)) return; + + const guildDocument = await GuildModel.findById(reaction.message.guildId); + + // check whether the message has required number of reactions + if (reaction.count < guildDocument.starboardThreshold) return; + // find the starboard channel + const starboardChannel = reaction.message.guild.channels.cache.get(guildDocument.starboardChannel) as GuildTextBasedChannel; + + // check whether starboard is enabled + if (!starboardChannel) return; + + // fetch the message + await reaction.message.fetch(); + + // check whether the message author is starring their own message + if (reaction.message.author?.id === user.id) return; + + // extract image attachment from the message + // although, it can be a video. + // TODO: find a way to filter out videos. + const imageAttachment = reaction.message.attachments.filter(a => Boolean(a.height && a.width)).first(); + + // check whether the message has any content + if (!reaction.message.content && !imageAttachment) return; + + // post the message in the starboard + await starboardChannel.send({ + embeds: [ + { + color: COLORS.YELLOW, + author: { + name: reaction.message.author?.tag, + icon_url: reaction.message.member?.displayAvatarURL(), + url: reaction.message.url, + }, + description: reaction.message.content, + image: { + url: imageAttachment?.url, + }, + footer: { + text: "Starboard", + }, + }, + ], + }); + + // update the starboard cache + if (guildStarboardCache instanceof Array) { + guildStarboardCache.push(reaction.message.id); + } else { + starboardCache.set(reaction.message.guildId, [ reaction.message.id ]); + } + memcache.set("starboard", starboardCache); + } +} + +export = MessageReactionAddListener; From b1ff4e1e685894dd851e3c9f5ee886b3f28273db Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Thu, 8 Dec 2022 20:06:18 +0530 Subject: [PATCH 08/14] version: 10.4.0 Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e1a01071..b84111806 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bastion", - "version": "10.3.0", + "version": "10.4.0", "description": "Get an enhanced Discord experience!", "homepage": "https://bastion.traction.one", "main": "./dist/index.js", From 477580d09f6ef8c66aeebd2316d72736f9fff905 Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Sat, 10 Dec 2022 18:26:34 +0530 Subject: [PATCH 09/14] locales(info): add new strings Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- locales/en-US/info.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/locales/en-US/info.yaml b/locales/en-US/info.yaml index dded3cb66..85178412d 100644 --- a/locales/en-US/info.yaml +++ b/locales/en-US/info.yaml @@ -1,5 +1,14 @@ about: "Bastion — It is a multipurpose bot that can help your community get an enhanced Discord experience! Let us know if you want it to have any features that can help your community." +autoThreadArchive: "This thread has been closed and locked." +autoThreadCreate: "This thread has been automatically created from your message in the %channel% channel." +autoThreadFirstMessageError: "This is either not an auto thread or the first message in this thread was deleted." +autoThreadName: "I've updated the name of the thread to **%name%**." +autoThreadNoPerms: "The command can only be used by the thread owner." +autoThreadsInvalidChannel: "Auto threads can only be enabled in Text channels." +autoThreadsDisable: "I've disabled auto threads in the server." +autoThreadsEnable: "I've enabled auto threads in the %channel% channel." changes: "See what's new in **%version%**" +commandThreadOnly: "This command can only be used in a thread." rewardsClaimed: "You've claimed your daily reward of **%amount% Bastion Coins**." rewardsAlreadyClaimed: "You've already claimed your daily reward. Check back tomorrow." donate: "Donate to support the development of Bastion to help keep it running, and enjoy an enhanced Bastion exprience." From 5d276adbe3a93ea978bc910628a6fdd446a9d502 Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Sat, 10 Dec 2022 18:27:25 +0530 Subject: [PATCH 10/14] models(Guild): add autoThreadChannels field Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/models/Guild.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/models/Guild.ts b/src/models/Guild.ts index b6dba6a3c..ad623f093 100644 --- a/src/models/Guild.ts +++ b/src/models/Guild.ts @@ -55,6 +55,7 @@ export interface Guild { suggestionsChannel?: string; reportsChannel?: string; streamerRole?: string; + autoThreadChannels?: string[]; votingChannels?: string[]; // twitch notifications twitchNotificationChannel?: string; @@ -201,6 +202,9 @@ export default mongoose.model("Guild", new mongoose.S unique: true, sparse: true, }, + autoThreadChannels: { + type: [ String ], + }, votingChannels: { type: [ String ], }, From f74cbd9f1d0040ebedca0ccfcf9e08c2789dbd28 Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Sat, 10 Dec 2022 18:45:44 +0530 Subject: [PATCH 11/14] commands(config): add autoThreads command Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/commands/config/autoThreads.ts | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/commands/config/autoThreads.ts diff --git a/src/commands/config/autoThreads.ts b/src/commands/config/autoThreads.ts new file mode 100644 index 000000000..f2a4cd4d1 --- /dev/null +++ b/src/commands/config/autoThreads.ts @@ -0,0 +1,51 @@ +/*! + * @author TRACTION (iamtraction) + * @copyright 2022 + */ +import { ChannelType, ChatInputCommandInteraction, PermissionFlagsBits } from "discord.js"; +import { Client, Command } from "@bastion/tesseract"; + +import GuildModel from "../../models/Guild"; + +class AutoThreadsCommand extends Command { + constructor() { + super({ + name: "auto-threads", + description: "Configure auto threads in the server.", + options: [], + userPermissions: [ PermissionFlagsBits.ManageGuild ], + }); + } + + public async exec(interaction: ChatInputCommandInteraction<"cached">): Promise { + await interaction.deferReply(); + + // check whether the channel is valid + if (interaction.channel.type !== ChannelType.GuildText) { + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "autoThreadsInvalidChannel")); + } + + const guildDocument = await GuildModel.findById(interaction.guildId); + + // disable auto threads + if (guildDocument.autoThreadChannels?.includes(interaction.channelId)) { + guildDocument.autoThreadChannels = []; + + await guildDocument.save(); + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "autoThreadsDisable")); + } + + // set rate limit before enabling auto threads + if (interaction.channel.rateLimitPerUser < 5 ) { + await interaction.channel.setRateLimitPerUser(5, "Auto Threads Channel"); + } + + // enable auto threads + guildDocument.autoThreadChannels = [ interaction.channelId ]; + + await guildDocument.save(); + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "autoThreadsEnable", { channel: interaction.channel })); + } +} + +export = AutoThreadsCommand; From a3b3730150da6175929357269daede1c46300871 Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Sat, 10 Dec 2022 18:46:46 +0530 Subject: [PATCH 12/14] commands(thread): add close command Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/commands/thread/close.ts | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/commands/thread/close.ts diff --git a/src/commands/thread/close.ts b/src/commands/thread/close.ts new file mode 100644 index 000000000..91f22779b --- /dev/null +++ b/src/commands/thread/close.ts @@ -0,0 +1,42 @@ +/*! + * @author TRACTION (iamtraction) + * @copyright 2022 + */ +import { ChatInputCommandInteraction } from "discord.js"; +import { Client, Command, Logger } from "@bastion/tesseract"; + +class ThreadCloseCommand extends Command { + constructor() { + super({ + name: "close", + description: "Close and lock the thread.", + options: [], + }); + } + + public async exec(interaction: ChatInputCommandInteraction<"cached">): Promise { + await interaction.deferReply({ ephemeral: true }); + + if (!interaction.channel.isThread()) { + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "commandThreadOnly")); + } + + const starterMessage = await interaction.channel.fetchStarterMessage().catch(Logger.error); + if (!starterMessage) { + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "autoThreadFirstMessageError")); + } + + if (starterMessage.author.id === interaction.user.id) { + await interaction.channel.edit({ + archived: true, + locked: true, + reason: `Requested by ${ interaction.user.tag }`, + }); + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "autoThreadArchive")); + } + + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "autoThreadNoPerms")); + } +} + +export = ThreadCloseCommand; From cc86609ad408d269809b8040f5ce2ec664c24a4a Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Sat, 10 Dec 2022 18:47:29 +0530 Subject: [PATCH 13/14] commands(thread): add name command Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/commands/thread/name.ts | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/commands/thread/name.ts diff --git a/src/commands/thread/name.ts b/src/commands/thread/name.ts new file mode 100644 index 000000000..2e28e6406 --- /dev/null +++ b/src/commands/thread/name.ts @@ -0,0 +1,46 @@ +/*! + * @author TRACTION (iamtraction) + * @copyright 2022 + */ +import { ApplicationCommandOptionType, ChatInputCommandInteraction } from "discord.js"; +import { Client, Command, Logger } from "@bastion/tesseract"; + +class ThreadNameCommand extends Command { + constructor() { + super({ + name: "name", + description: "Change the name of the thread.", + options: [ + { + type: ApplicationCommandOptionType.String, + name: "name", + description: "The new name for the thread.", + required: true, + }, + ], + }); + } + + public async exec(interaction: ChatInputCommandInteraction<"cached">): Promise { + await interaction.deferReply({ ephemeral: true }); + const name = interaction.options.getString("name"); + + if (!interaction.channel.isThread()) { + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "commandThreadOnly")); + } + + const starterMessage = await interaction.channel.fetchStarterMessage().catch(Logger.error); + if (!starterMessage) { + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "autoThreadFirstMessageError")); + } + + if (starterMessage.author.id === interaction.user.id) { + await interaction.channel.setName(name, `Requested by ${ interaction.user.tag }`); + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "autoThreadName", { name })); + } + + return await interaction.editReply((interaction.client as Client).locales.getText(interaction.guildLocale, "autoThreadNoPerms")); + } +} + +export = ThreadNameCommand; From 492e79e36a79adaa128301bc69c9ae0121337f54 Mon Sep 17 00:00:00 2001 From: TRACTION <19631364+iamtraction@users.noreply.github.com> Date: Sat, 10 Dec 2022 18:55:46 +0530 Subject: [PATCH 14/14] listeners(messageCreate): implement auto threads Signed-off-by: TRACTION <19631364+iamtraction@users.noreply.github.com> --- src/listeners/messageCreate.ts | 36 +++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/listeners/messageCreate.ts b/src/listeners/messageCreate.ts index 56939df50..a8e7e0691 100644 --- a/src/listeners/messageCreate.ts +++ b/src/listeners/messageCreate.ts @@ -2,7 +2,7 @@ * @author TRACTION (iamtraction) * @copyright 2022 */ -import { Message, Snowflake, Team } from "discord.js"; +import { ChannelType, Message, Snowflake, Team, ThreadAutoArchiveDuration } from "discord.js"; import { Client, Listener, Logger } from "@bastion/tesseract"; import GuildModel, { Guild as GuildDocument } from "../models/Guild"; @@ -186,6 +186,38 @@ class MessageCreateListener extends Listener<"messageCreate"> { } }; + handleAutoThreads = async (message: Message, guildDocument: GuildDocument): Promise => { + if (!message.content) return; + + // check whether the channel is valid + if (message.channel.type !== ChannelType.GuildText) return; + + // check whether channel has slow mode + if (!message.channel.rateLimitPerUser) return; + + // check whether auto threads is enabled in the channel + if (!guildDocument.autoThreadChannels?.includes(message.channelId)) return; + + // create a new thread + const thread = await message.channel.threads.create({ + type: ChannelType.PrivateThread, + name: message.member.displayName + " — " + new Date().toDateString(), + autoArchiveDuration: ThreadAutoArchiveDuration.OneDay, + reason: `Auto Thread for ${ message.author.tag }`, + invitable: true, + startMessage: message, + }); + + thread.send({ + content: `Hello ${ message.author }! +\nThis thread has been automatically created from your message in the ${ message.channel } channel. +\n**Useful Commands** +• \`/thread name\` — Change the name of the thread. +• \`/thread close\` — Close and lock the thread once you're done. +\n*This thread will be automatically archived after 24 hours of inactivity.*`, + }).catch(Logger.ignore); + }; + handleInstantResponses = async (message: Message): Promise => { if (!message.content) return; @@ -243,6 +275,8 @@ class MessageCreateListener extends Listener<"messageCreate"> { this.handleVotingChannel(message, guildDocument).catch(Logger.error); // karma this.handleKarma(message, guildDocument).catch(Logger.error); + // auto threads + this.handleAutoThreads(message, guildDocument).catch(Logger.error); } else { if (process.env.BASTION_RELAY_DMS || ((message.client as Client).settings as bastion.Settings)?.relayDirectMessages) { // relay direct messages