Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Fix race condition bug #100

Merged
merged 1 commit into from
Mar 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"description": "Needle is a discord bot that helps you manage your discord threads.",
"main": "./dist/index.js",
"scripts": {
"build": "rd /s /q dist & tsc",
"start": "npm run build && node ./dist/index.js",
"dev": "npm run build && node ./scripts/deploy-commands.js && node ./dist/index.js",
"build": "rd /s /q dist & tsc --sourceMap",
"start": "npm run build && node --enable-source-maps ./dist/index.js",
"dev": "npm run build && node ./scripts/deploy-commands.js && node --enable-source-maps ./dist/index.js",
"undeploy": "npm run build && node ./scripts/deploy-commands.js --undeploy",
"deploy": "npm run undeploy && node ./scripts/deploy-commands.js --global"
},
Expand Down
20 changes: 10 additions & 10 deletions src/commands/close.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ export const command: NeedleCommand = {
async execute(interaction: CommandInteraction | MessageComponentInteraction): Promise<void> {
const member = interaction.member;
if (!(member instanceof GuildMember)) {
return interactionReply(interaction, getMessage("ERR_UNKNOWN"));
return interactionReply(interaction, getMessage("ERR_UNKNOWN", interaction.id));
}

const channel = interaction.channel;
if (!channel?.isThread()) {
return interactionReply(interaction, getMessage("ERR_ONLY_IN_THREAD"));
return interactionReply(interaction, getMessage("ERR_ONLY_IN_THREAD", interaction.id));
}

// Invoking slash commands seem to unarchive the threads for now so ironically, this has no effect
// Leaving this in if Discord decides to change their API around this
if (channel.archived) {
return interactionReply(interaction, getMessage("ERR_NO_EFFECT"));
return interactionReply(interaction, getMessage("ERR_NO_EFFECT", interaction.id));
}

const hasManageThreadsPermissions = member.permissionsIn(channel).has(Permissions.FLAGS.MANAGE_THREADS, true);
Expand All @@ -59,11 +59,11 @@ export const command: NeedleCommand = {

const threadAuthor = await getThreadAuthor(channel);
if (!threadAuthor) {
return interactionReply(interaction, getMessage("ERR_AMBIGUOUS_THREAD_AUTHOR"));
return interactionReply(interaction, getMessage("ERR_AMBIGUOUS_THREAD_AUTHOR", interaction.id));
}

if (threadAuthor !== interaction.user) {
return interactionReply(interaction, getMessage("ERR_ONLY_THREAD_OWNER"));
return interactionReply(interaction, getMessage("ERR_ONLY_THREAD_OWNER", interaction.id));
}

await archiveThread(channel);
Expand All @@ -72,13 +72,13 @@ export const command: NeedleCommand = {
if (shouldArchiveImmediately(thread)) {
if (interaction.isButton()) {
await interaction.update({ content: interaction.message.content });
const message = getMessage("SUCCESS_THREAD_ARCHIVE_IMMEDIATE");
const message = getMessage("SUCCESS_THREAD_ARCHIVE_IMMEDIATE", interaction.id);
if (message) {
await thread.send(message);
}
}
else if (interaction.isCommand()) {
await interactionReply(interaction, getMessage("SUCCESS_THREAD_ARCHIVE_IMMEDIATE"), false);
await interactionReply(interaction, getMessage("SUCCESS_THREAD_ARCHIVE_IMMEDIATE", interaction.id), false);
}

await setEmojiForNewThread(thread, false);
Expand All @@ -87,21 +87,21 @@ export const command: NeedleCommand = {
}

if (thread.autoArchiveDuration === 60) {
return interactionReply(interaction, getMessage("ERR_NO_EFFECT"));
return interactionReply(interaction, getMessage("ERR_NO_EFFECT", interaction.id));
}

await setEmojiForNewThread(thread, false);
await thread.setAutoArchiveDuration(60);

if (interaction.isButton()) {
await interaction.update({ content: interaction.message.content });
const message = getMessage("SUCCESS_THREAD_ARCHIVE_SLOW");
const message = getMessage("SUCCESS_THREAD_ARCHIVE_SLOW", interaction.id);
if (message) {
await thread.send(message);
}
}
else if (interaction.isCommand()) {
await interactionReply(interaction, getMessage("SUCCESS_THREAD_ARCHIVE_SLOW"), false);
await interactionReply(interaction, getMessage("SUCCESS_THREAD_ARCHIVE_SLOW", interaction.id), false);
}
}
},
Expand Down
42 changes: 21 additions & 21 deletions src/commands/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,18 @@ export const command: NeedleCommand = {

async execute(interaction: CommandInteraction): Promise<void> {
if (!interaction.guildId || !interaction.guild) {
return interactionReply(interaction, getMessage("ERR_ONLY_IN_SERVER"));
return interactionReply(interaction, getMessage("ERR_ONLY_IN_SERVER", interaction.id));
}

if (!memberIsModerator(interaction.member as GuildMember)) {
return interactionReply(interaction, getMessage("ERR_INSUFFICIENT_PERMS"));
return interactionReply(interaction, getMessage("ERR_INSUFFICIENT_PERMS", interaction.id));
}

if (interaction.options.getSubcommand() === "default") {
const success = resetConfigToDefault(interaction.guild.id);
return interactionReply(interaction, success
? "Successfully reset the Needle configuration to the default."
: getMessage("ERR_NO_EFFECT"), !success);
: getMessage("ERR_NO_EFFECT", interaction.id), !success);
}

if (interaction.options.getSubcommand() === "emojis") {
Expand All @@ -152,22 +152,22 @@ export const command: NeedleCommand = {
return configureAutothreading(interaction);
}

return interactionReply(interaction, getMessage("ERR_UNKNOWN"));
return interactionReply(interaction, getMessage("ERR_UNKNOWN", interaction.id));
},
};

function configureEmojis(interaction: CommandInteraction): Promise<void> {
const enable = interaction.options.getBoolean("enabled");
if (enable === null || interaction.guild === null) {
return interactionReply(interaction, getMessage("ERR_PARAMETER_MISSING"));
return interactionReply(interaction, getMessage("ERR_PARAMETER_MISSING", interaction.id));
}

if (enable === emojisEnabled(interaction.guild)) {
return interactionReply(interaction, getMessage("ERR_NO_EFFECT"));
return interactionReply(interaction, getMessage("ERR_NO_EFFECT", interaction.id));
}

const success = setEmojisEnabled(interaction.guild, enable);
if (!success) return interactionReply(interaction, getMessage("ERR_UNKNOWN"));
if (!success) return interactionReply(interaction, getMessage("ERR_UNKNOWN", interaction.id));

return interactionReply(interaction, enable
? "Successfully enabled emojis."
Expand All @@ -179,17 +179,17 @@ function configureMessage(interaction: CommandInteraction): Promise<void> {
const value = interaction.options.getString("value");

if (!interaction.guild) {
return interactionReply(interaction, getMessage("ERR_ONLY_IN_SERVER"));
return interactionReply(interaction, getMessage("ERR_ONLY_IN_SERVER", interaction.id));
}

if (!value || value.length === 0) {
return interactionReply(interaction, `**${key}** message:\n\n>>> ${getMessage(key, false)}`);
return interactionReply(interaction, `**${key}** message:\n\n>>> ${getMessage(key, interaction.id, false)}`);
}

const oldValue = getMessage(key, false);
const oldValue = getMessage(key, interaction.id, false);
return setMessage(interaction.guild, key, value)
? interactionReply(interaction, `Changed **${key}**\n\nOld message:\n> ${oldValue?.replaceAll("\n", "\n> ")}\n\nNew message:\n>>> ${value}`, false)
: interactionReply(interaction, getMessage("ERR_UNKNOWN"));
: interactionReply(interaction, getMessage("ERR_UNKNOWN", interaction.id));
}

async function configureAutothreading(interaction: CommandInteraction): Promise<void> {
Expand All @@ -201,42 +201,42 @@ async function configureAutothreading(interaction: CommandInteraction): Promise<
const slowmode = parseInt(interaction.options.getString("slowmode") ?? "0");

if (!interaction.guild || !interaction.guildId) {
return interactionReply(interaction, getMessage("ERR_ONLY_IN_SERVER"));
return interactionReply(interaction, getMessage("ERR_ONLY_IN_SERVER", interaction.id));
}

if (!channel || enabled == null) {
return interactionReply(interaction, getMessage("ERR_PARAMETER_MISSING"));
return interactionReply(interaction, getMessage("ERR_PARAMETER_MISSING", interaction.id));
}

const clientUser = interaction.client.user;
if (!clientUser) return interactionReply(interaction, getMessage("ERR_UNKNOWN"));
if (!clientUser) return interactionReply(interaction, getMessage("ERR_UNKNOWN", interaction.id));

const botMember = await interaction.guild.members.fetch(clientUser);
const botPermissions = botMember.permissionsIn(channel.id);

if (!botPermissions.has(Permissions.FLAGS.VIEW_CHANNEL)) {
addMessageContext({ channel });
return interactionReply(interaction, getMessage("ERR_CHANNEL_VISIBILITY"));
addMessageContext(interaction.id, { channel });
return interactionReply(interaction, getMessage("ERR_CHANNEL_VISIBILITY", interaction.id));
}

if (slowmode && slowmode > 0 && !botPermissions.has(Permissions.FLAGS.MANAGE_THREADS)) {
addMessageContext({ channel });
return interactionReply(interaction, getMessage("ERR_CHANNEL_SLOWMODE"));
addMessageContext(interaction.id, { channel });
return interactionReply(interaction, getMessage("ERR_CHANNEL_SLOWMODE", interaction.id));
}

if (enabled) {
const success = enableAutothreading(interaction.guild, channel.id, includeBots, archiveImmediately, customMessage, slowmode);
return success
? interactionReply(interaction, `Updated auto-threading settings for <#${channel.id}>`, false)
: interactionReply(interaction, getMessage("ERR_UNKNOWN"));
: interactionReply(interaction, getMessage("ERR_UNKNOWN", interaction.id));
}

if (!isAutoThreadChannel(channel.id, interaction.guildId)) {
return interactionReply(interaction, getMessage("ERR_NO_EFFECT"));
return interactionReply(interaction, getMessage("ERR_NO_EFFECT", interaction.id));
}

const success = disableAutothreading(interaction.guild, channel.id);
return success
? interactionReply(interaction, `Removed auto-threading in <#${channel.id}>`, false)
: interactionReply(interaction, getMessage("ERR_UNKNOWN"));
: interactionReply(interaction, getMessage("ERR_UNKNOWN", interaction.id));
}
12 changes: 6 additions & 6 deletions src/commands/title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,22 @@ export const command: NeedleCommand = {
async execute(interaction: CommandInteraction): Promise<void> {
const member = interaction.member;
if (!(member instanceof GuildMember)) {
return interactionReply(interaction, getMessage("ERR_UNKNOWN"));
return interactionReply(interaction, getMessage("ERR_UNKNOWN", interaction.id));
}

const channel = interaction.channel;
if (!channel?.isThread()) {
return interactionReply(interaction, getMessage("ERR_ONLY_IN_THREAD"));
return interactionReply(interaction, getMessage("ERR_ONLY_IN_THREAD", interaction.id));
}

const newThreadName = interaction.options.getString("value");
if (!newThreadName) {
return interactionReply(interaction, getMessage("ERR_PARAMETER_MISSING"));
return interactionReply(interaction, getMessage("ERR_PARAMETER_MISSING", interaction.id));
}

const oldThreadName = channel.name;
if (oldThreadName === newThreadName) {
return interactionReply(interaction, getMessage("ERR_NO_EFFECT"));
return interactionReply(interaction, getMessage("ERR_NO_EFFECT", interaction.id));
}

const hasChangeTitlePermissions = member
Expand All @@ -72,11 +72,11 @@ export const command: NeedleCommand = {

const threadAuthor = await getThreadAuthor(channel);
if (!threadAuthor) {
return interactionReply(interaction, getMessage("ERR_AMBIGUOUS_THREAD_AUTHOR"));
return interactionReply(interaction, getMessage("ERR_AMBIGUOUS_THREAD_AUTHOR", interaction.id));
}

if (threadAuthor !== interaction.user) {
return interactionReply(interaction, getMessage("ERR_ONLY_THREAD_OWNER"));
return interactionReply(interaction, getMessage("ERR_ONLY_THREAD_OWNER", interaction.id));
}

await setThreadName(channel, newThreadName);
Expand Down
8 changes: 4 additions & 4 deletions src/handlers/commandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function handleCommandInteraction(interaction: CommandInteraction): Promi
}
catch (error) {
console.error(error);
return interactionReply(interaction, getMessage("ERR_UNKNOWN"));
return interactionReply(interaction, getMessage("ERR_UNKNOWN", interaction.id));
}
}

Expand All @@ -47,7 +47,7 @@ export async function handleButtonClickedInteraction(interaction: MessageCompone
}
catch (error) {
console.error(error);
return interactionReply(interaction, getMessage("ERR_UNKNOWN"));
return interactionReply(interaction, getMessage("ERR_UNKNOWN", interaction.id));
}
}

Expand All @@ -58,8 +58,8 @@ export async function getOrLoadAllCommands(allowCache = true): Promise<NeedleCom

console.log("Started reloading commands from disk.");

const commandFiles = await promises.readdir(COMMANDS_PATH);
commandFiles.filter(file => file.endsWith(".js"));
let commandFiles = await promises.readdir(COMMANDS_PATH);
commandFiles = commandFiles.filter(file => file.endsWith(".js"));
const output = [];
for (const file of commandFiles) {
const { command } = await import(`${COMMANDS_PATH}/${file}`);
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/interactionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { resetMessageContext, addMessageContext } from "../helpers/messageHelper
import { handleButtonClickedInteraction, handleCommandInteraction } from "./commandHandler";

export async function handleInteractionCreate(interaction: Interaction): Promise<void> {
addMessageContext({
addMessageContext(interaction.id, {
user: interaction.user,
interaction: interaction,
channel: interaction.channel ?? undefined,
Expand All @@ -33,5 +33,5 @@ export async function handleInteractionCreate(interaction: Interaction): Promise
await handleButtonClickedInteraction(interaction);
}

resetMessageContext();
resetMessageContext(interaction.id);
}
17 changes: 9 additions & 8 deletions src/handlers/messageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
//
// ________________________________________________________________________________________________

import { type Message, MessageActionRow, MessageButton, NewsChannel, TextChannel, ThreadChannel } from "discord.js";
import { type Message, MessageActionRow, MessageButton, NewsChannel, TextChannel, ThreadChannel, SnowflakeUtil, type Snowflake } from "discord.js";
import { emojisEnabled, getConfig, includeBotsForAutothread, getSlowmodeSeconds } from "../helpers/configHelpers";
import { getMessage, resetMessageContext, addMessageContext, isAutoThreadChannel, getHelpButton, replaceMessageVariables, getThreadAuthor } from "../helpers/messageHelpers";
import { getRequiredPermissions, getSafeDefaultAutoArchiveDuration } from "../helpers/permissionHelpers";
Expand All @@ -40,8 +40,9 @@ export async function handleMessageCreate(message: Message): Promise<void> {
return;
}

await autoCreateThread(message);
resetMessageContext();
const requestId = SnowflakeUtil.generate();
await autoCreateThread(message, requestId);
resetMessageContext(requestId);
}

async function updateTitle(thread: ThreadChannel, message: Message) {
Expand All @@ -53,7 +54,7 @@ async function updateTitle(thread: ThreadChannel, message: Message) {
await thread.setName(thread.name.replace("🆕", ""));
}

async function autoCreateThread(message: Message) {
async function autoCreateThread(message: Message, requestId: Snowflake) {
// Server outage
if (!message.guild?.available) return;

Expand Down Expand Up @@ -86,7 +87,7 @@ async function autoCreateThread(message: Message) {
return;
}

addMessageContext({
addMessageContext(requestId, {
user: authorUser,
channel: channel,
message: message,
Expand Down Expand Up @@ -119,8 +120,8 @@ async function autoCreateThread(message: Message) {

const overrideMessageContent = getConfig(guild.id).threadChannels?.find(x => x?.channelId === channel.id)?.messageContent;
const msgContent = overrideMessageContent
? replaceMessageVariables(overrideMessageContent)
: getMessage("SUCCESS_THREAD_CREATE");
? replaceMessageVariables(overrideMessageContent, requestId)
: getMessage("SUCCESS_THREAD_CREATE", requestId);

if (msgContent && msgContent.length > 0) {
await thread.send({
Expand All @@ -129,5 +130,5 @@ async function autoCreateThread(message: Message) {
});
}

resetMessageContext();
resetMessageContext(requestId);
}
24 changes: 0 additions & 24 deletions src/helpers/fileHelpers.ts

This file was deleted.

Loading