diff --git a/.version b/.version index 21e8796..ee90284 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -1.0.3 +1.0.4 diff --git a/README.md b/README.md index f3b5fd3..a68ac75 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,8 @@ The bot requires a handful of permissions to be granted when added to a server. - **Send Messages**: Required to reply to commands with a message - **Send Messages in Threads**: Required to send messages in threads - **Create Private Threads**: Required to create new private threads -- **Manage Threads**: Required to add/remove users from a thread, pin messages in threads, and close and archive threads +- **Manage Threads**: Required to add/remove users from a thread and close and archive threads +- **Mention @€veryone, @here, and All Roles**: Required to ping the casual and competitive roles - **Move Members**: Required to disconnect users from voice channels if kicked or dropped out ## Running locally diff --git a/src/client/discord-client.ts b/src/client/discord-client.ts index 5d8c1de..52e9f3e 100644 --- a/src/client/discord-client.ts +++ b/src/client/discord-client.ts @@ -23,7 +23,7 @@ export function createDiscordClient() { GatewayIntentBits.MessageContent, GatewayIntentBits.DirectMessages, ], - allowedMentions: { parse: ['roles'] }, + allowedMentions: { parse: ['roles', 'users', 'everyone'] }, partials: [Partials.Channel], }); } diff --git a/src/commands/create-command.ts b/src/commands/create-command.ts index 8d7e28f..83d4bbc 100644 --- a/src/commands/create-command.ts +++ b/src/commands/create-command.ts @@ -60,6 +60,7 @@ export async function handleCreateCommand( content: rolePing || undefined, embeds: [embed], components: [buttonRow, selectRow], + allowedMentions: { parse: ['roles', 'users', 'everyone'] }, }); const message = await reply.fetch(); const matchId = randomUUID(); diff --git a/src/commands/reping-command.ts b/src/commands/reping-command.ts index 69f64fb..6689aec 100644 --- a/src/commands/reping-command.ts +++ b/src/commands/reping-command.ts @@ -22,14 +22,13 @@ export async function handleRepingCommand( telemetry?: TelemetryService, ) { try { - await interaction.deferReply(); - const userId = interaction.user.id; const userEventId = eventManager.userOwnsEvent(userId); if (!userEventId) { - await interaction.editReply({ + await interaction.reply({ content: ERROR_MESSAGES.NO_EVENT_OWNED, + flags: ['Ephemeral'], }); return; } @@ -51,8 +50,9 @@ export async function handleRepingCommand( const remainingMs = TIMINGS.REPING_COOLDOWN_MS - timeSinceLastUse; const remainingMinutes = Math.ceil(remainingMs / 60000); - await interaction.editReply({ + await interaction.reply({ content: ERROR_MESSAGES.REPING_COOLDOWN(remainingMinutes), + flags: ['Ephemeral'], }); return; } @@ -60,8 +60,9 @@ export async function handleRepingCommand( const channelId = eventManager.getChannelId(userEventId); if (!channelId) { - await interaction.editReply({ + await interaction.reply({ content: ERROR_MESSAGES.CHANNEL_NOT_FOUND, + flags: ['Ephemeral'], }); return; } @@ -72,8 +73,9 @@ export async function handleRepingCommand( ); if (!channel || !channel.isTextBased()) { - await interaction.editReply({ + await interaction.reply({ content: ERROR_MESSAGES.CHANNEL_NO_ACCESS, + flags: ['Ephemeral'], }); return; } @@ -84,8 +86,9 @@ export async function handleRepingCommand( ); if (!message || !message.embeds[0]) { - await interaction.editReply({ + await interaction.reply({ content: ERROR_MESSAGES.MESSAGE_NOT_FOUND, + flags: ['Ephemeral'], }); return; } @@ -98,20 +101,24 @@ export async function handleRepingCommand( const missingPlayers = MAX_PARTICIPANTS - currentCount; if (currentCount === MAX_PARTICIPANTS) { - await interaction.editReply({ + await interaction.reply({ content: ERROR_MESSAGES.REPING_EVENT_FULL, + flags: ['Ephemeral'], }); return; } const rolePing = getPingsForServer(interaction, isCasual); if (!rolePing) { - await interaction.editReply({ + await interaction.reply({ content: ERROR_MESSAGES.ROLE_NOT_FOUND, + flags: ['Ephemeral'], }); return; } + await interaction.deferReply(); + await eventManager.deleteRepingMessageIfExists( userEventId, interaction.client, @@ -122,6 +129,7 @@ export async function handleRepingCommand( const reply = await interaction.editReply({ content: REPING_MESSAGE(rolePing, missingPlayers, messageUrl), + allowedMentions: { parse: ['roles', 'users', 'everyone'] }, }); const repingMessage = await withRetry( diff --git a/src/tests/commands/reping-command.test.ts b/src/tests/commands/reping-command.test.ts index f5fc947..dec31c8 100644 --- a/src/tests/commands/reping-command.test.ts +++ b/src/tests/commands/reping-command.test.ts @@ -52,6 +52,7 @@ describe('reping-command', () => { fetch: vi.fn().mockResolvedValue(mockChannel), }, }, + reply: vi.fn().mockResolvedValue(undefined), deferReply: vi.fn().mockResolvedValue(undefined), editReply: vi.fn().mockResolvedValue(mockReply), }; @@ -102,6 +103,8 @@ describe('reping-command', () => { }); it('should reject if user does not own event', async () => { + const mockReply = vi.fn().mockResolvedValue(undefined); + mockInteraction.reply = mockReply; mockEventManager.userOwnsEvent.mockReturnValue(false as never); await handleRepingCommand( @@ -110,12 +113,15 @@ describe('reping-command', () => { mockTelemetry as never, ); - expect(mockInteraction.editReply).toHaveBeenCalledWith({ + expect(mockReply).toHaveBeenCalledWith({ content: ERROR_MESSAGES.NO_EVENT_OWNED, + flags: ['Ephemeral'], }); }); it('should enforce cooldown period', async () => { + const mockReply = vi.fn().mockResolvedValue(undefined); + mockInteraction.reply = mockReply; const now = Date.now(); mockEventManager.getRepingCooldown.mockReturnValue( now - TIMINGS.REPING_COOLDOWN_MS / 2, @@ -127,14 +133,17 @@ describe('reping-command', () => { mockTelemetry as never, ); - expect(mockInteraction.editReply).toHaveBeenCalledWith( + expect(mockReply).toHaveBeenCalledWith( expect.objectContaining({ content: expect.stringContaining('wait'), + flags: ['Ephemeral'], }), ); }); it('should reject if event is full', async () => { + const mockReply = vi.fn().mockResolvedValue(undefined); + mockInteraction.reply = mockReply; const participants = new Map([ ['user1', { userId: 'user1', role: 'None', rank: null }], ['user2', { userId: 'user2', role: 'Tank', rank: null }], @@ -153,8 +162,9 @@ describe('reping-command', () => { mockTelemetry as never, ); - expect(mockInteraction.editReply).toHaveBeenCalledWith({ + expect(mockReply).toHaveBeenCalledWith({ content: ERROR_MESSAGES.REPING_EVENT_FULL, + flags: ['Ephemeral'], }); }); @@ -187,6 +197,8 @@ describe('reping-command', () => { }); it('should reject if channel not found', async () => { + const mockReply = vi.fn().mockResolvedValue(undefined); + mockInteraction.reply = mockReply; mockEventManager.getChannelId.mockReturnValue(undefined as never); await handleRepingCommand( @@ -195,8 +207,9 @@ describe('reping-command', () => { mockTelemetry as never, ); - expect(mockInteraction.editReply).toHaveBeenCalledWith({ + expect(mockReply).toHaveBeenCalledWith({ content: ERROR_MESSAGES.CHANNEL_NOT_FOUND, + flags: ['Ephemeral'], }); });