Skip to content

Commit bd8dba2

Browse files
authored
#609 & #561: consolidate thread handling behavior (#665)
* #609 & #561: consolidate thread handling behavior - cull duplicate fetches of bounty threads - implement bounty board thread getter to consistently handle thread and forum request misses - refactored log message logic to reduce duplicate detection logic and avoid awaiting requests known to be missing permissions * write up new convention * replace `threadCanReceiveMessages` with `ThreadChannel.prototype.sendable` * improvements from review - export `auditReasonBountyComplete` string instead of copy/pasting magic string - fix return santization logic in `getBountyBoardThread`
1 parent 1c2f3d1 commit bd8dba2

19 files changed

Lines changed: 284 additions & 198 deletions

ChangeLog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
## BountyBot Version 2.12.0fib:
33
- Added command `/bounty ping` to mention bounty hunters who have reacted to the bounty's thread or marked themselves interested in the bounty's event
44
- `/create-default bounty-board-forum` now has an option for customizing the new channel's name
5+
- Completing a bounty now applies ~~strikethrough~~ to its title on the bounty board
6+
- The following actions now sends record keeping messages to bounty board threads: showcasing a bounty, adding a thumbnail
7+
- Fixed several crashes related to missed fetches on the bounty board
58
- Fixed Critical Secondings adding the Seconder to the Toast's list of recipients
69
## BountyBot Version 2.11.1ib:
710
- Completing or taking down bounties now cancel the bounty's event if it hasn't been closed already

source/frontend/commands/bounty/complete.js

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const { MessageFlags, userMention, channelMention, bold, ModalBuilder, LabelBuilder, UserSelectMenuBuilder, StringSelectMenuBuilder } = require("discord.js");
1+
const { MessageFlags, userMention, channelMention, bold, ModalBuilder, LabelBuilder, UserSelectMenuBuilder, StringSelectMenuBuilder, PermissionFlagsBits, strikethrough } = require("discord.js");
22
const { timeConversion } = require("../../../shared");
3-
const { commandMention, bountyEmbed, goalCompletionEmbed, sendRewardMessage, syncRankRoles, unarchiveAndUnlockThread, rewardSummary, consolidateHunterReceipts, refreshReferenceChannelScoreboardSeasonal, refreshReferenceChannelScoreboardOverall, sentenceListEN, butIgnoreInteractionCollectorErrors, selectOptionsFromBounties, butIgnoreErrorIf, isUnknownGuildScheduledEventError, isMissingPermissionError } = require("../../shared");
3+
const { commandMention, bountyEmbed, goalCompletionEmbed, sendRewardMessage, syncRankRoles, unarchiveAndUnlockThread, rewardSummary, consolidateHunterReceipts, refreshReferenceChannelScoreboardSeasonal, refreshReferenceChannelScoreboardOverall, butIgnoreInteractionCollectorErrors, selectOptionsFromBounties, butIgnoreErrorIf, isUnknownGuildScheduledEventError, isMissingPermissionError, getBountyBoardThread, refreshBountyBoardThread, auditReasonBountyComplete } = require("../../shared");
44
const { SubcommandWrapper } = require("../../classes");
55
const { Company } = require("../../../database/models");
66
const { SKIP_INTERACTION_HANDLING } = require("../../../constants");
@@ -111,20 +111,19 @@ module.exports = new SubcommandWrapper("complete", "Close one of your open bount
111111
if (origin.company.bountyBoardId) {
112112
acknowledgeOptions.content += `${channelMention(bounty.postingId)}, was completed!`;
113113
modalSubmission.editReply(acknowledgeOptions);
114-
const bountyBoard = await modalSubmission.guild.channels.fetch(origin.company.bountyBoardId);
115-
bountyBoard.threads.fetch(bounty.postingId).then(async thread => {
116-
await unarchiveAndUnlockThread(thread, "bounty complete");
117-
thread.setAppliedTags([origin.company.bountyBoardCompletedTagId]);
118-
thread.send({ content: rewardMessageContent, flags: MessageFlags.SuppressNotifications });
119-
return thread.fetchStarterMessage();
120-
}).then(async posting => {
121-
posting.edit({
122-
embeds: [bountyEmbed(bounty, modalSubmission.member, origin.hunter.getLevel(origin.company.xpCoefficient), true, origin.company, new Set([...validatedHunters.keys()]), await bounty.getScheduledEvent(modalSubmission.guild.scheduledEvents), goalProgress)],
123-
components: []
124-
}).then(() => {
125-
posting.channel.setArchived(true, "bounty completed");
126-
});
127-
});
114+
115+
const auditLogReason = auditReasonBountyComplete;
116+
const bountyThread = await getBountyBoardThread(modalSubmission.guild, origin.company.bountyBoardId, bounty.postingId);
117+
if (bountyThread) {
118+
if (modalSubmission.guild.members.me.permissions.has(PermissionFlagsBits.ManageThreads)) {
119+
refreshBountyBoardThread(await bountyThread.fetchStarterMessage(), { embed: bountyEmbed(bounty, modalSubmission.member, origin.hunter.getLevel(origin.company.xpCoefficient), true, origin.company, new Set([...validatedHunters.keys()]), await bounty.getScheduledEvent(modalSubmission.guild.scheduledEvents), goalProgress), title: strikethrough(bounty.title) }, auditLogReason);
120+
await unarchiveAndUnlockThread(bountyThread, auditLogReason);
121+
}
122+
if (bountyThread.sendable) {
123+
bountyThread.send({ content: rewardMessageContent, flags: MessageFlags.SuppressNotifications });
124+
}
125+
bountyThread.edit({ archived: true, appliedTags: [origin.company.bountyBoardCompletedTagId], reason: auditLogReason });
126+
}
128127
} else {
129128
acknowledgeOptions.content += `${bold(bounty.title)}, was completed!`;
130129
modalSubmission.editReply(acknowledgeOptions).then(message => {

source/frontend/commands/bounty/edit.js

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const { ActionRowBuilder, StringSelectMenuBuilder, MessageFlags, ComponentType, unorderedList, bold } = require("discord.js");
1+
const { ActionRowBuilder, StringSelectMenuBuilder, MessageFlags, ComponentType, unorderedList, bold, PermissionFlagsBits } = require("discord.js");
22
const { SubcommandWrapper } = require("../../classes");
3-
const { textsHaveAutoModInfraction, commandMention, bountyEmbed, validateScheduledEventTimestamps, bountyScheduledEventPayload, editBountyModalAndSubmissionOptions, selectOptionsFromBounties, unarchiveAndUnlockThread, butIgnoreInteractionCollectorErrors } = require("../../shared");
3+
const { textsHaveAutoModInfraction, commandMention, bountyEmbed, validateScheduledEventTimestamps, bountyScheduledEventPayload, editBountyModalAndSubmissionOptions, selectOptionsFromBounties, unarchiveAndUnlockThread, butIgnoreInteractionCollectorErrors, getBountyBoardThread, refreshBountyBoardThread } = require("../../shared");
44
const { SKIP_INTERACTION_HANDLING } = require("../../../constants");
55

66
module.exports = new SubcommandWrapper("edit", "Edit the title, description, image, or time of one of your bounties",
@@ -92,22 +92,21 @@ module.exports = new SubcommandWrapper("edit", "Edit the title, description, ima
9292
}
9393
await bounty.update(updatePayload);
9494

95+
const embed = bountyEmbed(bounty, modalSubmission.member, origin.hunter.getLevel(origin.company.xpCoefficient), false, origin.company, await logicLayer.bounties.getHunterIdSet(bountyId), event);
96+
modalSubmission.update({ content: `Bounty edited! You can use ${commandMention("bounty showcase")} to let other bounty hunters know about the changes.`, embeds: [embed], components: [] });
97+
9598
// update bounty board
96-
const embeds = [bountyEmbed(bounty, modalSubmission.member, origin.hunter.getLevel(origin.company.xpCoefficient), false, origin.company, await logicLayer.bounties.getHunterIdSet(bountyId), event)];
97-
if (origin.company.bountyBoardId) {
98-
interaction.guild.channels.fetch(origin.company.bountyBoardId).then(bountyBoard => {
99-
return bountyBoard.threads.fetch(bounty.postingId);
100-
}).then(async thread => {
101-
await unarchiveAndUnlockThread(thread, "Unarchived to update posting");
102-
thread.edit({ name: bounty.title });
103-
thread.send({ content: "The bounty was edited.", flags: MessageFlags.SuppressNotifications });
104-
return thread.fetchStarterMessage();
105-
}).then(posting => {
106-
posting.edit({ embeds });
107-
})
99+
const auditLogReason = "bounty edited by poster";
100+
const bountyThread = await getBountyBoardThread(modalSubmission.guild, origin.company.bountyBoardId, bounty.postingId);
101+
if (bountyThread) {
102+
if (modalSubmission.guild.members.me.permissions.has(PermissionFlagsBits.ManageThreads)) {
103+
refreshBountyBoardThread(await bountyThread.fetchStarterMessage(), { embed }, auditLogReason);
104+
await unarchiveAndUnlockThread(bountyThread, auditLogReason);
105+
}
106+
if (bountyThread.sendable) {
107+
bountyThread.send({ content: "This bounty was edited.", flags: MessageFlags.SuppressNotifications });
108+
}
108109
}
109-
110-
modalSubmission.update({ content: `Bounty edited! You can use ${commandMention("bounty showcase")} to let other bounty hunters know about the changes.`, embeds, components: [] });
111110
});
112111
}).catch(butIgnoreInteractionCollectorErrors);
113112
}

source/frontend/commands/bounty/ping.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const { ModalBuilder, UserSelectMenuBuilder, TextInputBuilder, StringSelectMenuB
22
const { SKIP_INTERACTION_HANDLING } = require("../../../constants");
33
const { SubcommandWrapper } = require("../../classes");
44
const { bountyPing } = require("../../shared/flows/bountyPing");
5-
const { selectOptionsFromBounties, butIgnoreInteractionCollectorErrors } = require("../../shared");
5+
const { selectOptionsFromBounties, butIgnoreInteractionCollectorErrors, getBountyBoardThread } = require("../../shared");
66
const { timeConversion } = require("../../../shared");
77

88
module.exports = new SubcommandWrapper("ping", "Mention bounty hunters that reacted to your bounty's thread or event",
@@ -53,14 +53,7 @@ module.exports = new SubcommandWrapper("ping", "Mention bounty hunters that reac
5353
return;
5454
}
5555

56-
let bountyThread;
57-
if (origin.company.bountyBoardId && bounty.postingId) {
58-
const bountyBoard = await modalSubmission.guild.channels.fetch(origin.company.bountyBoardId);
59-
if (bountyBoard) {
60-
bountyThread = await bountyBoard.threads.fetch(bounty.postingId);
61-
}
62-
}
63-
56+
const bountyThread = await getBountyBoardThread(modalSubmission.guild, origin.company.bountyBoardId, bounty.postingId);
6457
bountyPing(modalSubmission, { message: labelIdMessage, excludedBountyHunters: labelIdExcludedBountyHunters }, bounty, bountyThread);
6558
}
6659
);

source/frontend/commands/bounty/record-turn-ins.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const { userMention, bold, MessageFlags, StringSelectMenuBuilder, UserSelectMenuBuilder, ModalBuilder, LabelBuilder } = require("discord.js");
1+
const { userMention, bold, MessageFlags, StringSelectMenuBuilder, UserSelectMenuBuilder, ModalBuilder, LabelBuilder, PermissionFlagsBits } = require("discord.js");
22
const { SubcommandWrapper } = require("../../classes");
3-
const { sentenceListEN, commandMention, refreshBountyThreadStarterMessage, randomCongratulatoryPhrase, selectOptionsFromBounties, butIgnoreUnknownChannelErrors, butIgnoreInteractionCollectorErrors } = require("../../shared");
3+
const { sentenceListEN, commandMention, randomCongratulatoryPhrase, selectOptionsFromBounties, butIgnoreInteractionCollectorErrors, getBountyBoardThread, bountyEmbed, unarchiveAndUnlockThread } = require("../../shared");
44
const { timeConversion } = require("../../../shared");
55
const { SKIP_INTERACTION_HANDLING } = require("../../../constants");
66

@@ -55,9 +55,16 @@ module.exports = new SubcommandWrapper("record-turn-ins", "Record turn-ins of on
5555
await logicLayer.bounties.bulkCreateCompletions(bounty.id, bounty.companyId, Array.from(eligibleTurnInIds), null);
5656
const newTurnInList = sentenceListEN(Array.from(newTurnInIds.values().map(id => userMention(id))));
5757
sentences.unshift(`Turn-ins of ${bold(bounty.title)} have been recorded for the following hunters: ${newTurnInList}`);
58-
const post = await refreshBountyThreadStarterMessage(modalSubmission.guild, origin.company, bounty, await bounty.getScheduledEvent(modalSubmission.guild.scheduledEvents), modalSubmission.member, origin.hunter.getLevel(origin.company.xpCoefficient), eligibleTurnInIds).catch(butIgnoreUnknownChannelErrors);
59-
if (post) {
60-
post.channel.send({ content: `${newTurnInList} ${newTurnInIds.size === 1 ? "has" : "have"} turned in this bounty! ${randomCongratulatoryPhrase()}!` });
58+
59+
const bountyThread = await getBountyBoardThread(modalSubmission.guild, origin.company.bountyBoardId, bounty.postingId);
60+
if (bountyThread) {
61+
if (modalSubmission.guild.members.me.permissions.has(PermissionFlagsBits.ManageThreads)) {
62+
(await bountyThread.fetchStarterMessage()).edit({ embeds: [bountyEmbed(bounty, modalSubmission.member, origin.hunter.getLevel(origin.company.xpCoefficient), false, origin.company, eligibleTurnInIds, await bounty.getScheduledEvent(modalSubmission.guild.scheduledEvents))] });
63+
await unarchiveAndUnlockThread(bountyThread, "bounty turn-ins recorded by poster");
64+
}
65+
if (bountyThread.sendable) {
66+
bountyThread.send({ content: `${newTurnInList} ${newTurnInIds.size === 1 ? "has" : "have"} turned in this bounty! ${randomCongratulatoryPhrase()}!` });
67+
}
6168
}
6269
}
6370

source/frontend/commands/bounty/revoke-turn-ins.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const { MessageFlags, userMention, bold, StringSelectMenuBuilder, UserSelectMenuBuilder, ModalBuilder, LabelBuilder } = require("discord.js");
1+
const { MessageFlags, userMention, bold, StringSelectMenuBuilder, UserSelectMenuBuilder, ModalBuilder, LabelBuilder, PermissionFlagsBits } = require("discord.js");
22
const { SubcommandWrapper } = require("../../classes");
3-
const { sentenceListEN, refreshBountyThreadStarterMessage, selectOptionsFromBounties, commandMention, butIgnoreUnknownChannelErrors, butIgnoreInteractionCollectorErrors } = require("../../shared");
3+
const { sentenceListEN, selectOptionsFromBounties, commandMention, butIgnoreInteractionCollectorErrors, getBountyBoardThread, bountyEmbed, unarchiveAndUnlockThread } = require("../../shared");
44
const { timeConversion } = require("../../../shared");
55
const { SKIP_INTERACTION_HANDLING } = require("../../../constants");
66

@@ -49,10 +49,15 @@ module.exports = new SubcommandWrapper("revoke-turn-ins", "Revoke the turn-ins o
4949
const mentionList = sentenceListEN(removedIds.map(id => userMention(id)));
5050
modalSubmission.reply({ content: `These bounty hunters' turn-ins of ${bold(bounty.title)} have been revoked: ${mentionList}`, flags: MessageFlags.Ephemeral });
5151

52-
const post = await refreshBountyThreadStarterMessage(modalSubmission.guild, origin.company, bounty, await bounty.getScheduledEvent(modalSubmission.guild.scheduledEvents), modalSubmission.member, origin.hunter.getLevel(origin.company.xpCoefficient), await logicLayer.bounties.getHunterIdSet(bounty.id))
53-
.catch(butIgnoreUnknownChannelErrors);
54-
if (post) {
55-
post.channel.send({ content: `${mentionList} ${removedIds.length === 1 ? "has had their turn-in" : "have had their turn-ins"} revoked.` });
52+
const bountyThread = await getBountyBoardThread(modalSubmission.guild, origin.company.bountyBoardId, bounty.postingId);
53+
if (bountyThread) {
54+
if (modalSubmission.guild.members.me.permissions.has(PermissionFlagsBits.ManageThreads)) {
55+
(await bountyThread.fetchStarterMessage()).edit({ embeds: [bountyEmbed(bounty, modalSubmission.member, origin.hunter.getLevel(origin.company.xpCoefficient), false, origin.company, await logicLayer.bounties.getHunterIdSet(bounty.id), await bounty.getScheduledEvent(modalSubmission.guild.scheduledEvents))] });
56+
await unarchiveAndUnlockThread(bountyThread, "bounty turn-ins revoked by poster");
57+
}
58+
if (bountyThread.sendable) {
59+
bountyThread.send({ content: `${mentionList} ${removedIds.length === 1 ? "has had their turn-in" : "have had their turn-ins"} revoked.` });
60+
}
5661
}
5762
}
5863
);

source/frontend/commands/bounty/showcase.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
const { StringSelectMenuBuilder, MessageFlags, TimestampStyles, ModalBuilder, TextDisplayBuilder, LabelBuilder } = require("discord.js");
1+
const { StringSelectMenuBuilder, MessageFlags, TimestampStyles, ModalBuilder, TextDisplayBuilder, LabelBuilder, PermissionFlagsBits } = require("discord.js");
22
const { SubcommandWrapper } = require("../../classes");
33
const { timeConversion, discordTimestamp } = require("../../../shared");
44
const { SKIP_INTERACTION_HANDLING } = require("../../../constants");
5-
const { selectOptionsFromBounties, bountyEmbed, refreshBountyThreadStarterMessage, unarchiveAndUnlockThread, butIgnoreInteractionCollectorErrors, butIgnoreUnknownChannelErrors } = require("../../shared");
5+
const { selectOptionsFromBounties, bountyEmbed, unarchiveAndUnlockThread, butIgnoreInteractionCollectorErrors, getBountyBoardThread } = require("../../shared");
66

77
module.exports = new SubcommandWrapper("showcase", "Show the embed for one of your existing bounties and increase the reward",
88
async function executeSubcommand(interaction, origin, runMode, logicLayer) {
@@ -57,15 +57,20 @@ module.exports = new SubcommandWrapper("showcase", "Show the embed for one of yo
5757

5858
bounty = await bounty.increment("showcaseCount");
5959
await origin.hunter.update({ lastShowcaseTimestamp: new Date() });
60-
const hunterIdSet = await logicLayer.bounties.getHunterIdSet(bountyId);
6160
const currentPosterLevel = origin.hunter.getLevel(origin.company.xpCoefficient);
62-
const bountyScheduledEvent = await bounty.getScheduledEvent(modalSubmission.guild.scheduledEvents);
63-
refreshBountyThreadStarterMessage(modalSubmission.guild, origin.company, bounty, bountyScheduledEvent, modalSubmission.member, currentPosterLevel, hunterIdSet)
64-
.catch(butIgnoreUnknownChannelErrors);
65-
await unarchiveAndUnlockThread(modalSubmission.channel, "bounty showcased");
66-
modalSubmission.reply({
67-
content: `${modalSubmission.member} increased the reward on their bounty!`,
68-
embeds: [bountyEmbed(bounty, modalSubmission.member, currentPosterLevel, false, origin.company, hunterIdSet, bountyScheduledEvent)]
69-
});
61+
const embed = bountyEmbed(bounty, modalSubmission.member, currentPosterLevel, false, origin.company, await logicLayer.bounties.getHunterIdSet(bountyId), await bounty.getScheduledEvent(modalSubmission.guild.scheduledEvents));
62+
const bountyThread = await getBountyBoardThread(modalSubmission.guild, origin.company.bountyBoardId, bounty.postingId);
63+
64+
modalSubmission.reply({ content: `${modalSubmission.member} increased the reward on their bounty!`, embeds: [embed] });
65+
66+
if (bountyThread) {
67+
if (modalSubmission.guild.members.me.permissions.has(PermissionFlagsBits.ManageThreads)) {
68+
(await bountyThread.fetchStarterMessage()).edit({ embeds: [embed] });
69+
await unarchiveAndUnlockThread(bountyThread, "bounty showcased by poster");
70+
}
71+
if (bountyThread.sendable) {
72+
bountyThread.send({ content: `${modalSubmission.member} increased the reward on this bounty!`, flags: MessageFlags.SuppressNotifications });
73+
}
74+
}
7075
}
7176
);

0 commit comments

Comments
 (0)