diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b3b405eb..c2306924a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Option to turn on/off the Discord role requirement for Praise givers #419 #434 #440 - Support multiple wallets: WalletConneect, Trust, Rainbow etc #424 +- More verbose output from the `/admin announce` command #317 #441 ### Fixed diff --git a/packages/discord-bot/src/commands/forward.ts b/packages/discord-bot/src/commands/forward.ts index a98f5517c..2362912bd 100644 --- a/packages/discord-bot/src/commands/forward.ts +++ b/packages/discord-bot/src/commands/forward.ts @@ -1,5 +1,4 @@ import { SlashCommandBuilder } from '@discordjs/builders'; -import { APIMessage } from 'discord-api-types/v9'; import logger from 'jet-logger'; import { forwardHandler } from '../handlers/forward'; import { Command } from '../interfaces/Command'; diff --git a/packages/discord-bot/src/commands/praise.ts b/packages/discord-bot/src/commands/praise.ts index d69355ac8..3a6803355 100644 --- a/packages/discord-bot/src/commands/praise.ts +++ b/packages/discord-bot/src/commands/praise.ts @@ -1,5 +1,4 @@ import { SlashCommandBuilder } from '@discordjs/builders'; -import { APIMessage } from 'discord-api-types/v9'; import logger from 'jet-logger'; import { praiseHandler } from '../handlers/praise'; import { Command } from '../interfaces/Command'; diff --git a/packages/discord-bot/src/handlers/announce.ts b/packages/discord-bot/src/handlers/announce.ts index 9991ba3a9..80db3e155 100644 --- a/packages/discord-bot/src/handlers/announce.ts +++ b/packages/discord-bot/src/handlers/announce.ts @@ -107,6 +107,10 @@ export const announcementHandler: CommandHandler = async (interaction) => { break; } case 'continue': { + await interaction.editReply({ + content: 'Sending…', + components: [], + }); await selectTargets( interaction, selectedUserType, diff --git a/packages/discord-bot/src/interfaces/FailedToDmUsersList.ts b/packages/discord-bot/src/interfaces/FailedToDmUsersList.ts new file mode 100644 index 000000000..14af7809b --- /dev/null +++ b/packages/discord-bot/src/interfaces/FailedToDmUsersList.ts @@ -0,0 +1,6 @@ +export interface FailedToDmUsersList { + invalidUsers: string[]; + notFoundUsers: string[]; + closedDmUsers: string[]; + unknownErrorUsers: string[]; +} diff --git a/packages/discord-bot/src/utils/dmTargets.ts b/packages/discord-bot/src/utils/dmTargets.ts index ac19b5102..3c8d46b4c 100644 --- a/packages/discord-bot/src/utils/dmTargets.ts +++ b/packages/discord-bot/src/utils/dmTargets.ts @@ -7,9 +7,11 @@ import { PeriodDocument, PeriodDetailsQuantifierDto, } from 'api/dist/period/types'; +import { FailedToDmUsersList } from 'src/interfaces/FailedToDmUsersList'; -import { CommandInteraction } from 'discord.js'; +import { CommandInteraction, DiscordAPIError } from 'discord.js'; import { PraiseModel } from 'api/dist/praise/entities'; +import { Buffer } from 'node:buffer'; const sendDMs = async ( interaction: CommandInteraction, @@ -17,7 +19,12 @@ const sendDMs = async ( message: string ): Promise => { const successful = []; - const failed = []; + const failed: FailedToDmUsersList = { + invalidUsers: [], + notFoundUsers: [], + closedDmUsers: [], + unknownErrorUsers: [], + }; if (!users || users.length === 0) { await interaction.editReply( 'Message not sent. No recipients matched filter.' @@ -28,30 +35,111 @@ const sendDMs = async ( const userAccount = await UserAccountModel.findOne({ user: user._id, }); - const userId: string = userAccount?.accountId || 'oops'; + const userId: string = userAccount?.accountId || 'Unknown user'; + const userName: string = userAccount?.name || userId; try { const discordUser = await interaction.guild?.members.fetch(userId); if (!discordUser) { - failed.push(`<@!${userId}`); + failed.notFoundUsers.push(userAccount?.name || userId); continue; } await discordUser.send(message); - successful.push(`<@!${userId}>`); + successful.push(`${discordUser.user.tag}`); } catch (err) { - failed.push(`<@!${userId}>`); + const error = err as Error; + if (error instanceof DiscordAPIError) { + /* The numbers used below are status codes from discord's API. + * (ref - https://discord.com/developers/docs/topics/opcodes-and-status-codes#json) + */ + const discordErrorCode = (err as DiscordAPIError).code; + switch (discordErrorCode) { + case 50035: + failed.invalidUsers.push(userName); + break; + case 50007: + failed.closedDmUsers.push(userName); + break; + default: + failed.unknownErrorUsers.push(userAccount?.name || userId); + break; + } + } else { + failed.unknownErrorUsers.push(userAccount?.name || userId); + } } } - const failedMsg = `Announcement could not be delivered to ${failed.length} users.`; + + const failedCount = + failed.invalidUsers.length + + failed.notFoundUsers.length + + failed.closedDmUsers.length + + failed.unknownErrorUsers.length; + const failedMsg = `Announcement could not be delivered to ${failedCount} users.`; const successMsg = `Announcement successfully delivered to ${successful.length} recipients.`; const content = successful.length === 0 ? failedMsg - : failed.length === 0 + : failedCount === 0 ? successMsg : successMsg + '\n' + failedMsg; + + // TODO - Create a utility function to tabularise this data neatly + let summary = 'User\t\t\tStatus\t\tReason\n'; + successful.forEach((username: string) => { + summary += `${ + username.length <= 24 + ? username + new String(' ').repeat(24 - username.length) + : username.slice(0, 21) + ' ' + }Delivered\t-\n${ + username.length > 24 ? username.slice(21, username.length) + '\n' : '' + }`; + }); + failed.invalidUsers.forEach((username: string) => { + summary += `${ + username.length <= 24 + ? username + new String(' ').repeat(24 - username.length) + : username.slice(0, 21) + ' ' + }Not Delivered\tInvalid User\n${ + username.length > 24 ? username.slice(21, username.length) + '\n' : '' + }`; + }); + failed.notFoundUsers.forEach((username: string) => { + summary += `${ + username.length <= 24 + ? username + new String(' ').repeat(24 - username.length) + : username.slice(0, 21) + ' ' + }Not Delivered\tUser Not Found In Discord\n${ + username.length > 24 ? username.slice(21, username.length) + '\n' : '' + }`; + }); + failed.closedDmUsers.forEach((username: string) => { + summary += `${ + username.length <= 24 + ? username + new String(' ').repeat(24 - username.length) + : username.slice(0, 21) + ' ' + }Not Delivered\tDMs closed\n${ + username.length > 24 ? username.slice(21, username.length) + '\n' : '' + }`; + }); + failed.unknownErrorUsers.forEach((username: string) => { + summary += `${ + username.length <= 24 + ? username + new String(' ').repeat(24 - username.length) + : username.slice(0, 21) + ' ' + }Not Delivered\tUnknown Error\n${ + username.length > 24 ? username.slice(21, username.length) + '\n' : '' + }`; + }); + await interaction.editReply({ content: content, components: [], + files: [ + { + attachment: Buffer.from(summary, 'utf8'), + name: 'announcement_summary.txt', + }, + ], }); };