Skip to content

Commit

Permalink
Merge pull request #975 from TheBastionBot/dev
Browse files Browse the repository at this point in the history
v10.4
  • Loading branch information
iamtraction authored Dec 11, 2022
2 parents 88fb752 + 492e79e commit 6f26246
Show file tree
Hide file tree
Showing 12 changed files with 293 additions and 11 deletions.
9 changes: 9 additions & 0 deletions locales/en-US/info.yaml
Original file line number Diff line number Diff line change
@@ -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."
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/bastion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const bastion = new Client({
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMessages,
// GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildMessageReactions,
// GatewayIntentBits.GuildMessageTyping,
GatewayIntentBits.DirectMessages,
// GatewayIntentBits.DirectMessageReactions,
Expand Down
4 changes: 2 additions & 2 deletions src/commands/channel/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
],
Expand Down Expand Up @@ -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,
});

Expand Down
51 changes: 51 additions & 0 deletions src/commands/config/autoThreads.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> {
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;
8 changes: 8 additions & 0 deletions src/commands/config/starboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ],
});
Expand All @@ -27,11 +33,13 @@ class StarboardCommand extends Command {
public async exec(interaction: ChatInputCommandInteraction<"cached">): Promise<unknown> {
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` : "" }.`);
Expand Down
42 changes: 42 additions & 0 deletions src/commands/thread/close.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> {
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;
46 changes: 46 additions & 0 deletions src/commands/thread/name.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> {
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;
36 changes: 35 additions & 1 deletion src/listeners/messageCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -186,6 +186,38 @@ class MessageCreateListener extends Listener<"messageCreate"> {
}
};

handleAutoThreads = async (message: Message<true>, guildDocument: GuildDocument): Promise<void> => {
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<void> => {
if (!message.content) return;

Expand Down Expand Up @@ -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
Expand Down
83 changes: 83 additions & 0 deletions src/listeners/messageReactionAdd.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
// 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<Snowflake, Snowflake[]> || new Map<Snowflake, Snowflake[]>();
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;
Loading

0 comments on commit 6f26246

Please sign in to comment.