Skip to content

Commit 72cbd1a

Browse files
authored
#498 pt 2: crash fixes (#612)
* #498 pt 2: crash fixes - fix crash if `/bounty record-turn-in` modal expires - fix crash in `/bounty record-turn-in` if thread is deleted mid flow - fix crashes in `/bounty revoke-turn-in` and bounty control pannel revoke turn-in option - modal expiration - bounty is deleted mid flow - thread is deleted mid flow - refactored `/bounty revoke-turn-in` to use modal instead of message components - fix error in `/bounty take-down` if bounty thread was not found - fixed crash in bounty control panel record turn-in on modal expiration - confirmed posting thread deletion prevents modal submission - culled unnecessary channel fetch in bounty control panel record turn-in * label typo fix * #498 pt3: crash fixes in showcase flows (#615) * #498 pt3: fix crashes in showcase flows - refactor showcase flows to use modals instead of message components - fix unknown channel error on edit bounty thread crash in showcase slash flow - early out of control panel record/revoke turn-in if bounty isn't open * use modal instead of message components in `/evergreen complete` (#617) * use modal instead of message components in `/evergreen complete` - typing fix in incrementSeasonStat - fix missing import in dAPISerializers * replace reloadHunterMapSubset with getCompanyHunterMap (#618) * replace reloadHunterMapSubset with getCompanyHunterMap - uses 1 findAll vs multiple reloads - in case of flows with multiple input points, better to get full map fresh as it will also contain newly created hunters and hunters updated by other transactions * use modal instead of message components in `/evergreen swap` (#619) * use modal instead of message components in `/evergreen swap` - fix incorrect modal component for image in `/evergreen post` * consolidate input into modal in `/evergreen showcase` (#620) * consolidate input into modal in `/evergreen showcase` - disambiguate variable name in modal submission filter from `interaction` * fix `/bounty take-down` crash if user has no bounties (#621) * fix `/bounty take-down` crash if user has no bounties * bounty control panel completion improvements (#622) * bounty control panel completion improvements - control panel completion now accepts same-flow turn-ins - fix `/bounty take-down` attempting to delete all posting threads if used on bounty without thread - bounty embed turn-in field name changes based on bounty state - fixed potential contributions reporting "0 GP gained" when a goal isn't running - disabled bounty control panel showcases in threads * #498 pt4: crash fixes in swap flows (#623) * #498 pt4: crash fixes in swap flows - refactored `/bounty swap` and control panel swap to use modals - added "selected slot no longer owned" validation - added/cleaned up base reward listing for bounty slots in slot select - added early out to `/bounty swap` if user only has 1 bounty slot * skip fetching own thread and starter message in control panel edit (#624) * refactor `/bounty complete` to use modal (#625) * refactor `/bounty complete` to use modal - select menu with bounties instead of slot number text input doesn't require users to remember what slot number their bounty is in - added early out if user doesn't have any open bounties - moved bounty board thread update to after reply to reduce dAPI timeouts * remove setMaxValues(1) from StringSelects (already default) (#626) * remove setMaxValues(1) from StringSelects (already default) * bounty take-down improvements (#627) - moderation take-downs no longer delete bounty row in database - poster take-downs no longer update bounty row in database (which would be deleted later) - use formatter instead of markdown in moderation take-down
1 parent 216f5b0 commit 72cbd1a

25 files changed

Lines changed: 838 additions & 806 deletions

ChangeLog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
### Premium Features
77
- Added premium commands `/festival start-gp` and `/festival close-gp` for toggling times where gp contributions are multiplied (old festival commands renamed to `/festival start-xp` and `/festival close-xp`)
88
- Added the `nickname` option to `/config-premium` to allow bot managers to set a nickname for BountyBot that won't get cleared by toggling festivals on or off
9-
### Bug Fixes
9+
### Other Changes
10+
- The completing a bounty via the select in the bounty board can now add turn-ins like the slash command
1011
- Fixed Goal Point contributions not showing up in reward messages
1112
### Known Issues
1213
- When editing a bounty, not submitting a description, image, or timestamp pair, always deletes those data from the bounty (this will be fixed with modal checkboxes in the future)
Lines changed: 73 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,141 @@
1-
const { MessageFlags, userMention, channelMention, bold } = require("discord.js");
1+
const { MessageFlags, userMention, channelMention, bold, ModalBuilder, LabelBuilder, UserSelectMenuBuilder, StringSelectMenuBuilder } = require("discord.js");
22
const { timeConversion } = require("../../../shared");
3-
const { commandMention, bountyEmbed, goalCompletionEmbed, sendRewardMessage, reloadHunterMapSubset, syncRankRoles, unarchiveAndUnlockThread, rewardSummary, consolidateHunterReceipts, refreshReferenceChannelScoreboardSeasonal, refreshReferenceChannelScoreboardOverall } = require("../../shared");
3+
const { commandMention, bountyEmbed, goalCompletionEmbed, sendRewardMessage, syncRankRoles, unarchiveAndUnlockThread, rewardSummary, consolidateHunterReceipts, refreshReferenceChannelScoreboardSeasonal, refreshReferenceChannelScoreboardOverall, sentenceListEN, butIgnoreInteractionCollectorErrors, selectOptionsFromBounties } = require("../../shared");
44
const { SubcommandWrapper } = require("../../classes");
55
const { Company } = require("../../../database/models");
6+
const { SKIP_INTERACTION_HANDLING } = require("../../../constants");
67

78
module.exports = new SubcommandWrapper("complete", "Close one of your open bounties, distributing rewards to hunters who turned it in",
89
async function executeSubcommand(interaction, origin, runMode, logicLayer) {
9-
const slotNumber = interaction.options.getInteger("bounty-slot");
10-
const bounty = await logicLayer.bounties.findBounty({ userId: interaction.user.id, slotNumber, companyId: interaction.guild.id });
11-
if (!bounty) {
12-
interaction.reply({ content: "You don't have a bounty in the `bounty-slot` provided.", flags: MessageFlags.Ephemeral });
10+
const openBounties = await logicLayer.bounties.findOpenBounties(origin.user.id, origin.company.id);
11+
if (openBounties.length < 1) {
12+
interaction.reply({ content: "You don't appear to have any open bounties in this server.", flags: MessageFlags.Ephemeral });
13+
return;
14+
}
15+
16+
const labelIdBountyId = "bounty-id";
17+
const labelIdBountyHunters = "hunters";
18+
const maxHunters = 10;
19+
const modal = new ModalBuilder().setCustomId(`${SKIP_INTERACTION_HANDLING}${interaction.id}`)
20+
.setTitle("Mark your Bounty Complete!")
21+
.addLabelComponents(
22+
new LabelBuilder().setLabel("Bounty")
23+
.setStringSelectMenuComponent(
24+
new StringSelectMenuBuilder().setCustomId(labelIdBountyId)
25+
.setPlaceholder("Select a bounty...")
26+
.setOptions(selectOptionsFromBounties(openBounties))
27+
),
28+
new LabelBuilder().setLabel("Extra Turn-Ins")
29+
.setUserSelectMenuComponent(
30+
new UserSelectMenuBuilder().setCustomId(labelIdBountyHunters)
31+
.setPlaceholder(`Select up to ${maxHunters} bounty hunters...`)
32+
.setMaxValues(maxHunters)
33+
.setRequired(false)
34+
)
35+
);
36+
await interaction.showModal(modal);
37+
const modalSubmission = await interaction.awaitModalSubmit({ filter: incoming => incoming.customId === modal.data.custom_id, time: timeConversion(5, "m", "ms") })
38+
.catch(butIgnoreInteractionCollectorErrors);
39+
if (!modalSubmission) {
40+
return;
41+
}
42+
43+
const bounty = await logicLayer.bounties.findBounty(modalSubmission.fields.getStringSelectValues(labelIdBountyId)[0]);
44+
if (bounty?.state !== "open") {
45+
modalSubmission.reply({ content: "Your selected bounty no longer appears to be open.", flags: MessageFlags.Ephemeral });
1346
return;
1447
}
1548

1649
// disallow completion within 5 minutes of creating bounty
1750
if (runMode === "production" && new Date() < new Date(new Date(bounty.createdAt) + timeConversion(5, "m", "ms"))) {
18-
interaction.reply({ content: `Bounties cannot be completed within 5 minutes of their posting. You can ${commandMention("bounty add-completers")} so you won't forget instead.`, flags: MessageFlags.Ephemeral });
51+
modalSubmission.reply({ content: `Bounties cannot be completed within 5 minutes of their posting. You can ${commandMention("bounty record-turn-ins")} so you won't forget instead.`, flags: MessageFlags.Ephemeral });
1952
return;
2053
}
2154

55+
// Early-out if no completers
2256
const completions = await logicLayer.bounties.findBountyCompletions(bounty.id);
23-
const hunterCollection = await interaction.guild.members.fetch({ user: completions.map(reciept => reciept.userId) });
24-
for (const optionKey of ["first-bounty-hunter", "second-bounty-hunter", "third-bounty-hunter", "fourth-bounty-hunter", "fifth-bounty-hunter"]) {
25-
const guildMember = interaction.options.getMember(optionKey);
26-
if (guildMember) {
27-
if (guildMember?.id !== interaction.user.id && !hunterCollection.has(guildMember.id)) {
28-
hunterCollection.set(guildMember.id, guildMember);
29-
}
30-
}
31-
}
32-
57+
const memberCollection = await modalSubmission.guild.members.fetch({ user: completions.map(reciept => reciept.userId) });
3358
const validatedHunters = new Map();
34-
for (const [memberId, member] of hunterCollection) {
59+
for (const [memberId, member] of memberCollection) {
3560
if (runMode !== "production" || !member.user.bot) {
36-
const { hunter: [hunter] } = await logicLayer.hunters.findOrCreateBountyHunter(memberId, interaction.guild.id);
61+
const { hunter: [hunter] } = await logicLayer.hunters.findOrCreateBountyHunter(memberId, origin.company.id);
3762
if (!hunter.isBanned) {
3863
validatedHunters.set(memberId, hunter);
3964
}
4065
}
4166
}
4267

68+
const extraTurnIns = modalSubmission.fields.getSelectedMembers(labelIdBountyHunters);
69+
if (extraTurnIns !== null) {
70+
for (const [memberId, member] of extraTurnIns) {
71+
if (runMode !== "production" || !(member.user.bot || validatedHunters.has(memberId))) {
72+
const { hunter: [hunter] } = await logicLayer.hunters.findOrCreateBountyHunter(memberId, origin.company.id);
73+
if (!hunter.isBanned) {
74+
validatedHunters.set(memberId, hunter);
75+
}
76+
}
77+
}
78+
}
4379
if (validatedHunters.size < 1) {
44-
interaction.reply({ content: `No bounty hunters have turn-ins recorded for this bounty. If you'd like to close your bounty without distributng rewards, use ${commandMention("bounty take-down")}.`, flags: MessageFlags.Ephemeral })
80+
modalSubmission.reply({ content: `There aren't any eligible pending turn-ins for this bounty. If you'd like to close your bounty without paying out rewards, use ${commandMention("bounty take-down")}.`, flags: MessageFlags.Ephemeral })
4581
return;
4682
}
4783

48-
await interaction.deferReply();
84+
await modalSubmission.deferReply();
4985

5086
const season = await logicLayer.seasons.incrementSeasonStat(bounty.companyId, "bountiesCompleted");
5187

52-
let hunterMap = await logicLayer.hunters.getCompanyHunterMap(interaction.guild.id);
88+
let hunterMap = await logicLayer.hunters.getCompanyHunterMap(origin.company.id);
5389

5490
const previousCompanyLevel = Company.getLevel(origin.company.getXP(hunterMap));
5591
const hunterReceipts = await logicLayer.bounties.completeBounty(bounty, origin.hunter, validatedHunters, season, origin.company);
5692
const { companyReceipt, goalProgress } = await logicLayer.goals.progressGoal(origin.company, "bounties", origin.hunter, season);
57-
companyReceipt.guildName = interaction.guild.name;
58-
hunterMap = await reloadHunterMapSubset(hunterMap, [...validatedHunters.keys(), origin.hunter.userId]);
93+
companyReceipt.guildName = modalSubmission.guild.name;
94+
hunterMap = await logicLayer.hunters.getCompanyHunterMap(origin.company.id);
5995
const currentCompanyLevel = Company.getLevel(origin.company.getXP(hunterMap));
6096
if (previousCompanyLevel < currentCompanyLevel) {
6197
companyReceipt.levelUp = currentCompanyLevel;
6298
}
63-
const descendingRanks = await logicLayer.ranks.findAllRanks(interaction.guild.id);
99+
const descendingRanks = await logicLayer.ranks.findAllRanks(origin.company.id);
64100
const participationMap = await logicLayer.seasons.getParticipationMap(season.id);
65-
const seasonalHunterReceipts = await logicLayer.seasons.updatePlacementsAndRanks(participationMap, descendingRanks, await interaction.guild.roles.fetch());
66-
syncRankRoles(seasonalHunterReceipts, descendingRanks, interaction.guild.members);
101+
const seasonalHunterReceipts = await logicLayer.seasons.updatePlacementsAndRanks(participationMap, descendingRanks, await modalSubmission.guild.roles.fetch());
102+
syncRankRoles(seasonalHunterReceipts, descendingRanks, modalSubmission.guild.members);
67103
consolidateHunterReceipts(hunterReceipts, seasonalHunterReceipts);
68-
const content = rewardSummary("bounty", companyReceipt, hunterReceipts, origin.company.maxSimBounties);
104+
const rewardMessageContent = rewardSummary("bounty", companyReceipt, hunterReceipts, origin.company.maxSimBounties);
69105

70106
const acknowledgeOptions = { content: `${userMention(bounty.userId)}'s bounty, ` };
71107
if (goalProgress.goalCompleted) {
72108
acknowledgeOptions.embeds = [goalCompletionEmbed(goalProgress.contributorIds)];
73109
}
74110

75111
if (origin.company.bountyBoardId) {
76-
const bountyBoard = await interaction.guild.channels.fetch(origin.company.bountyBoardId);
112+
acknowledgeOptions.content += `${channelMention(bounty.postingId)}, was completed!`;
113+
modalSubmission.editReply(acknowledgeOptions);
114+
const bountyBoard = await modalSubmission.guild.channels.fetch(origin.company.bountyBoardId);
77115
bountyBoard.threads.fetch(bounty.postingId).then(async thread => {
78116
await unarchiveAndUnlockThread(thread, "bounty complete");
79117
thread.setAppliedTags([origin.company.bountyBoardCompletedTagId]);
80-
thread.send({ content, flags: MessageFlags.SuppressNotifications });
118+
thread.send({ content: rewardMessageContent, flags: MessageFlags.SuppressNotifications });
81119
return thread.fetchStarterMessage();
82120
}).then(async posting => {
83121
posting.edit({
84-
embeds: [bountyEmbed(bounty, interaction.member, origin.hunter.getLevel(origin.company.xpCoefficient), true, origin.company, new Set([...validatedHunters.keys()]), await bounty.getScheduledEvent(interaction.guild.scheduledEvents), goalProgress)],
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)],
85123
components: []
86124
}).then(() => {
87125
posting.channel.setArchived(true, "bounty completed");
88126
});
89127
});
90-
acknowledgeOptions.content += `${channelMention(bounty.postingId)}, was completed!`;
91-
interaction.editReply(acknowledgeOptions);
92128
} else {
93129
acknowledgeOptions.content += `${bold(bounty.title)}, was completed!`;
94-
interaction.editReply(acknowledgeOptions).then(message => {
95-
sendRewardMessage(message, content, `${bounty.title} Rewards`);
130+
modalSubmission.editReply(acknowledgeOptions).then(message => {
131+
sendRewardMessage(message, rewardMessageContent, `${bounty.title} Rewards`);
96132
})
97133
}
98134

99135
if (origin.company.scoreboardIsSeasonal) {
100-
refreshReferenceChannelScoreboardSeasonal(origin.company, interaction.guild, participationMap, descendingRanks, goalProgress);
136+
refreshReferenceChannelScoreboardSeasonal(origin.company, modalSubmission.guild, participationMap, descendingRanks, goalProgress);
101137
} else {
102-
refreshReferenceChannelScoreboardOverall(origin.company, interaction.guild, hunterMap, goalProgress);
138+
refreshReferenceChannelScoreboardOverall(origin.company, modalSubmission.guild, hunterMap, goalProgress);
103139
}
104140
}
105-
).setOptions(
106-
{
107-
type: "Integer",
108-
name: "bounty-slot",
109-
description: "The slot number of your bounty",
110-
required: true
111-
},
112-
{
113-
type: "User",
114-
name: "first-bounty-hunter",
115-
description: "A bounty hunter who turned in the bounty",
116-
required: false
117-
},
118-
{
119-
type: "User",
120-
name: "second-bounty-hunter",
121-
description: "A bounty hunter who turned in the bounty",
122-
required: false
123-
},
124-
{
125-
type: "User",
126-
name: "third-bounty-hunter",
127-
description: "A bounty hunter who turned in the bounty",
128-
required: false
129-
},
130-
{
131-
type: "User",
132-
name: "fourth-bounty-hunter",
133-
description: "A bounty hunter who turned in the bounty",
134-
required: false
135-
},
136-
{
137-
type: "User",
138-
name: "fifth-bounty-hunter",
139-
description: "A bounty hunter who completed the bounty",
140-
required: false
141-
}
142141
);

source/frontend/commands/bounty/edit.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ module.exports = new SubcommandWrapper("edit", "Edit the title, description, ima
1717
new ActionRowBuilder().addComponents(
1818
new StringSelectMenuBuilder().setCustomId(`${SKIP_INTERACTION_HANDLING}${interaction.id}`)
1919
.setPlaceholder("Select a bounty to edit...")
20-
.setMaxValues(1)
2120
.setOptions(selectOptionsFromBounties(openBounties))
2221
)
2322
],

source/frontend/commands/bounty/post.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ module.exports = new SubcommandWrapper("post", "Post your own bounty (+1 XP)",
3535
new ActionRowBuilder().addComponents(
3636
new StringSelectMenuBuilder().setCustomId(`${SKIP_INTERACTION_HANDLING}${interaction.id}`)
3737
.setPlaceholder("XP awarded depends on slot used...")
38-
.setMaxValues(1)
3938
.setOptions(slotOptions)
4039
)
4140
],

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

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

@@ -32,7 +32,12 @@ module.exports = new SubcommandWrapper("record-turn-ins", "Record turn-ins of on
3232
)
3333
);
3434
await interaction.showModal(modal);
35-
const modalSubmission = await interaction.awaitModalSubmit({ filter: interaction => interaction.customId === modal.data.custom_id, time: timeConversion(5, "m", "ms") });
35+
const modalSubmission = await interaction.awaitModalSubmit({ filter: incoming => incoming.customId === modal.data.custom_id, time: timeConversion(5, "m", "ms") })
36+
.catch(butIgnoreInteractionCollectorErrors);
37+
if (!modalSubmission) {
38+
return;
39+
}
40+
3641
const bounty = await logicLayer.bounties.findBounty(modalSubmission.fields.getStringSelectValues(labelIdBountyId)[0]);
3742
if (!bounty || bounty.state !== "open") {
3843
modalSubmission.reply({ content: "Your selected bounty could not be found.", flags: MessageFlags.Ephemeral });
@@ -50,7 +55,7 @@ module.exports = new SubcommandWrapper("record-turn-ins", "Record turn-ins of on
5055
await logicLayer.bounties.bulkCreateCompletions(bounty.id, bounty.companyId, Array.from(eligibleTurnInIds), null);
5156
const newTurnInList = sentenceListEN(Array.from(newTurnInIds.values().map(id => userMention(id))));
5257
sentences.unshift(`Turn-ins of ${bold(bounty.title)} have been recorded for the following hunters: ${newTurnInList}`);
53-
const post = await refreshBountyThreadStarterMessage(modalSubmission.guild, origin.company, bounty, await bounty.getScheduledEvent(modalSubmission.guild.scheduledEvents), modalSubmission.member, origin.hunter.getLevel(origin.company.xpCoefficient), eligibleTurnInIds);
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);
5459
if (post) {
5560
post.channel.send({ content: `${newTurnInList} ${newTurnInIds.size === 1 ? "has" : "have"} turned in this bounty! ${randomCongratulatoryPhrase()}!` });
5661
}

0 commit comments

Comments
 (0)