diff --git a/.version b/.version index 238d6e8..b0f3d96 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -1.0.7 +1.0.8 diff --git a/src/client/shutdown.ts b/src/client/shutdown.ts index af4c49c..06ed196 100644 --- a/src/client/shutdown.ts +++ b/src/client/shutdown.ts @@ -1,20 +1,11 @@ import type { Client } from 'discord.js'; -import { EmbedBuilder } from 'discord.js'; -import { - AUTHOR_ID, - COLORS, - FIELD_NAMES, - STATUS_MESSAGES, - TIMESTAMP, - TIMINGS, -} from '../constants.js'; +import { AUTHOR_ID, TIMESTAMP, TIMINGS } from '../constants.js'; import { cleanupEvent } from '../event/event-lifecycle.js'; import type { EventManager } from '../event/event-manager.js'; import type { ThreadManager } from '../managers/thread-manager.js'; import type { VoiceChannelManager } from '../managers/voice-channel-manager.js'; import { stopMetricsServer } from '../telemetry/metrics.js'; import type { TelemetryService } from '../telemetry/telemetry.js'; -import { updateEmbedField } from '../utils/embed-utils.js'; import { ErrorSeverity, handleError } from '../utils/error-handler.js'; import { MEDIUM_RETRY_OPTIONS, withRetryOrNull } from '../utils/retry.js'; @@ -78,33 +69,8 @@ export async function gracefulShutdown( `Updating event message ${i + 1}/${allTimers.length}: ${eventId}`, ); if (channelId) { - const channel = await withRetryOrNull( - () => client.channels.fetch(channelId), - MEDIUM_RETRY_OPTIONS, - ); - - if (channel?.isTextBased() && !channel.isDMBased()) { - const message = await withRetryOrNull( - () => channel.messages.fetch(eventId), - MEDIUM_RETRY_OPTIONS, - ); - - if (message) { - const embed = EmbedBuilder.from(message.embeds[0]).setColor( - COLORS.CANCELLED, - ); - - updateEmbedField( - embed, - FIELD_NAMES.STATUS, - STATUS_MESSAGES.SHUTDOWN, - ); - await withRetryOrNull( - () => message.edit({ embeds: [embed], components: [] }), - MEDIUM_RETRY_OPTIONS, - ); - } - } + eventManager.setTerminalState(eventId, 'shutdown'); + await eventManager.queueUpdate(eventId, true); } console.log(`Cleaning up event ${i + 1}/${allTimers.length}: ${eventId}`); diff --git a/src/commands/create-command.ts b/src/commands/create-command.ts index 9732a26..09da1ca 100644 --- a/src/commands/create-command.ts +++ b/src/commands/create-command.ts @@ -43,7 +43,14 @@ export async function handleCreateCommand( const buttonRow = createEventButtons(timeInMinutes); const selectRow = createRoleSelectMenu(); + const rankId = getExcaliburRankOfUser( + interaction.guild?.id, + interaction.member as GuildMember, + ); + const embed = createEventEmbed( + interaction.guild?.id, + rankId, interaction.user.username, interaction.user.displayAvatarURL(), interaction.user.id, @@ -65,11 +72,9 @@ export async function handleCreateCommand( eventManager.setCreator(message.id, interaction.user.id); eventManager.setMatchId(message.id, matchId); eventManager.setChannelId(message.id, message.channelId); - await eventManager.removeUserFromAllQueues( - interaction.user.id, - interaction.client, - telemetry, - ); + eventManager.setMessageData(message.id, casual, info); + + await eventManager.removeUserFromAllQueues(interaction.user.id, telemetry); eventManager.setTimer(message.id, { startTime, @@ -87,10 +92,7 @@ export async function handleCreateCommand( { userId: interaction.user.id, role: WEAPON_ROLES[0], - rank: getExcaliburRankOfUser( - interaction.guild?.id, - interaction.member as GuildMember, - ), + rank: rankId, }, ], ]), diff --git a/src/commands/kick-command.ts b/src/commands/kick-command.ts index 2d9cd8e..47e6865 100644 --- a/src/commands/kick-command.ts +++ b/src/commands/kick-command.ts @@ -1,18 +1,10 @@ -import { - type ChatInputCommandInteraction, - EmbedBuilder, - type TextChannel, -} from 'discord.js'; +import type { ChatInputCommandInteraction, TextChannel } from 'discord.js'; import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants.js'; import { promoteNextFromQueue } from '../event/event-lifecycle.js'; import type { EventManager } from '../event/event-manager.js'; import type { ThreadManager } from '../managers/thread-manager.js'; import type { VoiceChannelManager } from '../managers/voice-channel-manager.js'; import type { TelemetryService } from '../telemetry/telemetry.js'; -import { - updateParticipantFields, - updateQueueField, -} from '../utils/embed-utils.js'; import { ErrorSeverity, handleError } from '../utils/error-handler.js'; import { checkProcessingStates, @@ -134,14 +126,7 @@ export async function handleKickCommand( const updatedParticipants = eventManager.getParticipants(userEventId); if (timerData && updatedParticipants) { - const embed = EmbedBuilder.from(message.embeds[0]); - - updateParticipantFields(embed, updatedParticipants); - - const queue = eventManager.getQueue(userEventId); - updateQueueField(embed, queue); - - await message.edit({ embeds: [embed] }); + eventManager.queueUpdate(userEventId); } telemetry?.trackUserKicked({ diff --git a/src/constants.ts b/src/constants.ts index 82b6491..c894a15 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -29,9 +29,9 @@ export const TIMINGS = { HOUR_IN_MS: 60 * 60 * 1000, DAY_IN_MS: 24 * 60 * 60 * 1000, PROCESSING_TIMEOUT_MS: 30000, - EVENT_START_DELAY_MINUTES: DEV ? 0 : 0, SHUTDOWN_EVENT_CLEANUP_DELAY_MS: 2000, REPING_COOLDOWN_MS: 15 * 60 * 1000, + MESSAGE_UPDATE_DEBOUNCE_MS: 300, } as const; export const TIME_UNITS = { @@ -188,26 +188,38 @@ export const EXCALIBUR_RANKS = { '1': { name: 'TX Grandmaster', id: '1429217994168598669', + emoteName: 'Ex8s1_grandmaster', + emoteId: '1429215523824078941', }, '2': { name: 'T1 Legend', id: '1428998361188532264', + emoteName: 'Ex8s2_legend', + emoteId: '1428988098892529787', }, '3': { name: 'T2 Ascendant', id: '1428997469303341166', + emoteName: 'Ex8s3_ascendant', + emoteId: '1428988084535427102', }, '4': { name: 'T3 Elite', id: '1428997715106332815', + emoteName: 'Ex8s4_elite', + emoteId: '1428988071059259392', }, '5': { name: 'T4 Knight', id: '1428998081126596618', + emoteName: 'Ex8s5_knight', + emoteId: '1428988053472415768', }, '6': { name: 'T5 Squire', id: '1428998419250286704', + emoteName: 'Ex8s6_novice', + emoteId: '1428988037383327825', }, } as const; diff --git a/src/event/event-lifecycle.ts b/src/event/event-lifecycle.ts index dd82652..68762eb 100644 --- a/src/event/event-lifecycle.ts +++ b/src/event/event-lifecycle.ts @@ -1,39 +1,20 @@ +import type { Client, Guild, Message, TextChannel } from 'discord.js'; +import { EmbedBuilder } from 'discord.js'; import { - type Client, - EmbedBuilder, - type Guild, - type Message, - type TextChannel, -} from 'discord.js'; -import { - COLORS, - FIELD_NAMES, MATCH_ID_LENGTH, MAX_PARTICIPANTS, - START_MESSAGES, - STATUS_MESSAGES, TIMINGS, WEAPON_ROLES, } from '../constants.js'; import type { ThreadManager } from '../managers/thread-manager.js'; import type { VoiceChannelManager } from '../managers/voice-channel-manager.js'; import type { TelemetryService } from '../telemetry/telemetry.js'; -import { - createEventStartedButtons, - createRoleSelectMenu, - updateEmbedField, -} from '../utils/embed-utils.js'; import { ErrorSeverity, handleError } from '../utils/error-handler.js'; import { checkProcessingStates, getExcaliburRankOfUser, } from '../utils/helpers.js'; -import { - LOW_RETRY_OPTIONS, - MEDIUM_RETRY_OPTIONS, - withRetry, - withRetryOrNull, -} from '../utils/retry.js'; +import { LOW_RETRY_OPTIONS, withRetryOrNull } from '../utils/retry.js'; import type { EventManager, ParticipantMap } from './event-manager.js'; export async function startEvent( @@ -62,23 +43,7 @@ export async function startEvent( eventManager.deleteTimeout(message.id); } - const embed = EmbedBuilder.from(message.embeds[0]).setColor(COLORS.STARTED); - - updateEmbedField(embed, FIELD_NAMES.STATUS, STATUS_MESSAGES.STARTED); - updateEmbedField( - embed, - FIELD_NAMES.START, - START_MESSAGES.AT_TIME(Date.now()), - ); - - const buttonRow = createEventStartedButtons(); - const selectRow = createRoleSelectMenu(); - - await withRetry( - () => - message.edit({ embeds: [embed], components: [buttonRow, selectRow] }), - MEDIUM_RETRY_OPTIONS, - ); + eventManager.queueUpdate(message.id); const participants = Array.from(participantMap.values()); const channel = message.channel as TextChannel; @@ -214,53 +179,20 @@ export async function cleanupStaleEvents( const channelId = eventManager.getChannelId(messageId); const guildId = eventManager.getGuildId(messageId); - let message: Message | null = null; - - if (channelId) { - try { - const channel = await withRetryOrNull( - () => appClient.channels.fetch(channelId), - LOW_RETRY_OPTIONS, - ); + eventManager.setTerminalState(messageId, 'expired'); + await eventManager.queueUpdate(messageId, true); - if (channel?.isTextBased() && !channel.isDMBased()) { - message = await withRetryOrNull( - () => channel.messages.fetch(messageId), - LOW_RETRY_OPTIONS, - ); - } - } catch (error) { - handleError({ - reason: 'Failed to fetch stale event message', - severity: ErrorSeverity.LOW, - error, - metadata: { messageId, channelId }, - }); - } - } - if (message) { - const embed = EmbedBuilder.from(message.embeds[0]).setColor( - COLORS.CANCELLED, - ); - updateEmbedField(embed, FIELD_NAMES.STATUS, STATUS_MESSAGES.EXPIRED); + const matchId = eventManager.getMatchId(messageId); + const participants = eventManager.getParticipants(messageId); - await withRetryOrNull( - () => message.edit({ embeds: [embed], components: [] }), - LOW_RETRY_OPTIONS, - ); - - const matchId = eventManager.getMatchId(messageId); - const participants = eventManager.getParticipants(messageId); - - telemetry?.trackEventExpired({ - guildId: guildId || message.guild?.id || 'unknown', - eventId: messageId, - userId: appClient.user?.id || 'unknown', - participants: Array.from((participants || new Map()).values()), - channelId: message.channelId, - matchId: matchId || 'unknown', - }); - } + telemetry?.trackEventExpired({ + guildId: guildId || 'unknown', + eventId: messageId, + userId: appClient.user?.id || 'unknown', + participants: Array.from((participants || new Map()).values()), + channelId: channelId || 'unknown', + matchId: matchId || 'unknown', + }); } catch (error) { handleError({ reason: `Failed to process stale event ${messageId}`, @@ -294,13 +226,11 @@ export async function createEventStartTimeout( eventManager.deleteTimeout(message.id); } - const embed = EmbedBuilder.from(message.embeds[0]); - updateEmbedField( - embed, - FIELD_NAMES.START, - START_MESSAGES.AT_TIME(Date.now() + timeInMinutes * TIMINGS.MINUTE_IN_MS), - ); - await message.edit({ embeds: [embed] }); + const timerData = eventManager.getTimer(message.id); + if (timerData) { + timerData.duration = timeInMinutes * TIMINGS.MINUTE_IN_MS; + eventManager.queueUpdate(message.id); + } const timeout = setTimeout(async () => { try { @@ -326,13 +256,10 @@ export async function createEventStartTimeout( telemetry, ); } else { - const embed = EmbedBuilder.from(message.embeds[0]); - embed.setColor(COLORS.OPEN); - - updateEmbedField(embed, FIELD_NAMES.START, START_MESSAGES.WHEN_FULL); - updateEmbedField(embed, FIELD_NAMES.STATUS, STATUS_MESSAGES.OPEN); - - await message.edit({ embeds: [embed] }); + if (timerData) { + timerData.duration = undefined; + } + eventManager.queueUpdate(message.id); } } catch (error) { handleError({ @@ -373,7 +300,7 @@ export async function promoteNextFromQueue( rank: getExcaliburRankOfUser(guild.id, member), }); - await eventManager.removeUserFromAllQueues(nextUserId, appClient, telemetry); + await eventManager.removeUserFromAllQueues(nextUserId, telemetry); const threadId = eventManager.getThread(messageId); if (threadId) { diff --git a/src/event/event-manager.ts b/src/event/event-manager.ts index 78ede6c..a6d37bb 100644 --- a/src/event/event-manager.ts +++ b/src/event/event-manager.ts @@ -1,8 +1,22 @@ import { type Client, EmbedBuilder } from 'discord.js'; -import { TIMINGS } from '../constants.js'; +import { + COLORS, + FIELD_NAMES, + MAX_PARTICIPANTS, + PARTICIPANT_FIELD_NAME, + START_MESSAGES, + STATUS_MESSAGES, + TIMINGS, + TITLES, +} from '../constants.js'; import type { TelemetryService } from '../telemetry/telemetry.js'; -import { updateQueueField } from '../utils/embed-utils.js'; +import { + createEventButtons, + createEventStartedButtons, + createRoleSelectMenu, +} from '../utils/embed-utils.js'; import { ErrorSeverity, handleError } from '../utils/error-handler.js'; +import { getEmoteForRank } from '../utils/helpers.js'; import { LOW_RETRY_OPTIONS, withRetry, @@ -45,6 +59,16 @@ export class EventManager { private repingMessages = new Map(); private queues = new Map(); + private casual = new Map(); + private info = new Map(); + private updateTimeouts = new Map(); + private terminalStates = new Map< + string, + 'cancelled' | 'finished' | 'expired' | 'shutdown' | null + >(); + + constructor(private client: Client) {} + getParticipants(eventId: string) { return this.participants.get(eventId); } @@ -336,65 +360,25 @@ export class EventManager { return next; } - async removeUserFromAllQueues( - userId: string, - client: Client, - telemetry?: TelemetryService, - ) { + async removeUserFromAllQueues(userId: string, telemetry?: TelemetryService) { for (const [eventId, queue] of this.queues.entries()) { if (!queue.includes(userId)) continue; const filtered = queue.filter((id) => id !== userId); this.queues.set(eventId, filtered); - - const channelId = this.getChannelId(eventId); - if (!channelId) continue; - - try { - const channel = await withRetryOrNull( - () => client.channels.fetch(channelId), - LOW_RETRY_OPTIONS, - ); - - if (!channel || !channel.isTextBased()) continue; - - const message = await withRetryOrNull( - () => channel.messages.fetch(eventId), - LOW_RETRY_OPTIONS, - ); - - if (!message || !message.embeds[0]) continue; - - const embed = EmbedBuilder.from(message.embeds[0]); - updateQueueField(embed, filtered); - - await withRetry( - () => message.edit({ embeds: [embed] }), - LOW_RETRY_OPTIONS, - ); - - await telemetry?.trackUserLeftQueue({ - guildId: this.getGuildId(eventId) || 'unknown', - eventId: eventId, - userId: userId, - participants: Array.from( - (this.getParticipants(eventId) || new Map()).values(), - ), - channelId: channelId, - matchId: this.getMatchId(eventId) || 'unknown', - }); - } catch (error) { - handleError({ - reason: 'Failed to update queue field after removing user', - severity: ErrorSeverity.LOW, - error, - metadata: { - eventId, - userId, - }, - }); - } + this.queueUpdate(eventId); + + await telemetry?.trackUserLeftQueue({ + guildId: this.getGuildId(eventId) || 'unknown', + eventId: eventId, + userId: userId, + participants: Array.from( + (this.getParticipants(eventId) || new Map()).values(), + ), + channelId: this.getChannelId(eventId) || 'unknown', + matchId: this.getMatchId(eventId) || 'unknown', + }); } } @@ -416,10 +400,218 @@ export class EventManager { this.deleteRepingMessage(eventId); this.deleteQueue(eventId); + this.casual.delete(eventId); + this.info.delete(eventId); + this.terminalStates.delete(eventId); + const timeout = this.getTimeout(eventId); if (timeout) { clearTimeout(timeout); this.deleteTimeout(eventId); } + + const updateTimeout = this.updateTimeouts.get(eventId); + if (updateTimeout) { + clearTimeout(updateTimeout); + this.updateTimeouts.delete(eventId); + } + } + + setMessageData(eventId: string, casual: boolean, info?: string) { + this.casual.set(eventId, casual); + this.info.set(eventId, info); + } + + setTerminalState( + eventId: string, + state: 'cancelled' | 'finished' | 'expired' | 'shutdown', + ) { + this.terminalStates.set(eventId, state); + } + + getTerminalState(eventId: string) { + return this.terminalStates.get(eventId); + } + + buildEmbed(eventId: string): EmbedBuilder | null { + const participantMap = this.getParticipants(eventId); + const timerData = this.getTimer(eventId); + const creatorId = this.getCreator(eventId); + const queue = this.getQueue(eventId); + const casualMode = this.casual.get(eventId) ?? true; + const infoText = this.info.get(eventId); + + if (!participantMap || !timerData || !creatorId) return null; + + const creator = this.client.users.cache.get(creatorId); + const username = creator?.username || 'unknown'; + const avatarUrl = creator?.displayAvatarURL() || ''; + + const participants = Array.from(participantMap.values()); + const participantCount = participants.length; + + const terminalState = this.getTerminalState(eventId); + + let color: string = COLORS.OPEN; + let status: string = STATUS_MESSAGES.OPEN; + + if (terminalState) { + switch (terminalState) { + case 'cancelled': + color = COLORS.CANCELLED; + status = STATUS_MESSAGES.CANCELLED; + break; + case 'finished': + color = COLORS.FINISHED; + status = STATUS_MESSAGES.FINISHED; + break; + case 'expired': + color = COLORS.CANCELLED; + status = STATUS_MESSAGES.EXPIRED; + break; + case 'shutdown': + color = COLORS.CANCELLED; + status = STATUS_MESSAGES.SHUTDOWN; + break; + } + } else if (timerData.hasStarted && participantCount === MAX_PARTICIPANTS) { + color = COLORS.STARTED; + status = STATUS_MESSAGES.STARTED; + } else if (participantCount === MAX_PARTICIPANTS) { + status = STATUS_MESSAGES.READY; + } + + let startMessage: string = START_MESSAGES.WHEN_FULL; + if (timerData.hasStarted) { + startMessage = START_MESSAGES.AT_TIME(timerData.startTime); + } else if (timerData.duration) { + const startTimestamp = timerData.startTime + timerData.duration; + startMessage = START_MESSAGES.AT_TIME(startTimestamp); + } + + const participantList = participants + .map( + (p) => + `- ${getEmoteForRank(this.getGuildId(eventId), p.rank)}<@${p.userId}>`, + ) + .join('\n'); + const roleList = participants + .map((p) => `- ${p.role || 'None'}`) + .join('\n'); + + const embed = new EmbedBuilder() + .setAuthor({ + name: username, + iconURL: avatarUrl, + }) + .setTitle(casualMode ? TITLES.CASUAL : TITLES.COMPETITIVE) + .addFields([ + { + name: PARTICIPANT_FIELD_NAME(participantCount), + value: participantList, + inline: true, + }, + { + name: FIELD_NAMES.ROLE, + value: roleList, + inline: true, + }, + { + name: FIELD_NAMES.START, + value: startMessage, + inline: false, + }, + { + name: FIELD_NAMES.STATUS, + value: status, + inline: false, + }, + ]) + .setColor(color as `#${string}`); + + if (infoText) { + embed.setDescription(infoText); + } + + if (queue.length > 0) { + const queueValue = queue.map((userId) => `- <@${userId}>`).join('\n'); + embed.addFields({ + name: FIELD_NAMES.QUEUE, + value: queueValue, + inline: false, + }); + } + + return embed; + } + + buildComponents(eventId: string) { + const timerData = this.getTimer(eventId); + const terminalState = this.getTerminalState(eventId); + + if (!timerData || terminalState) return []; + + if (timerData.hasStarted) { + return [createEventStartedButtons(), createRoleSelectMenu()]; + } + + const timeInMinutes = timerData.duration + ? timerData.duration / TIMINGS.MINUTE_IN_MS + : undefined; + return [createEventButtons(timeInMinutes)]; + } + + queueUpdate(eventId: string, immediate = false) { + const existingTimeout = this.updateTimeouts.get(eventId); + if (existingTimeout) { + clearTimeout(existingTimeout); + } + + const performUpdate = async () => { + try { + this.updateTimeouts.delete(eventId); + + const channelId = this.getChannelId(eventId); + if (!channelId) return; + + const channel = await withRetryOrNull( + () => this.client.channels.fetch(channelId), + LOW_RETRY_OPTIONS, + ); + if (!channel || !channel.isTextBased()) return; + + const message = await withRetryOrNull( + () => channel.messages.fetch(eventId), + LOW_RETRY_OPTIONS, + ); + if (!message) return; + + const embed = this.buildEmbed(eventId); + const components = this.buildComponents(eventId); + if (!embed) return; + + await withRetry( + () => message.edit({ embeds: [embed], components }), + LOW_RETRY_OPTIONS, + ); + } catch (error) { + handleError({ + reason: 'Failed to update event message', + severity: ErrorSeverity.LOW, + error, + metadata: { eventId }, + }); + } + }; + + if (immediate) { + return performUpdate(); + } else { + const timeout = setTimeout( + performUpdate, + TIMINGS.MESSAGE_UPDATE_DEBOUNCE_MS, + ); + this.updateTimeouts.set(eventId, timeout); + } } } diff --git a/src/index.ts b/src/index.ts index b58ea6c..4cf3fe0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -98,10 +98,10 @@ const commands = [ ]; const telemetry = initializeTelemetry(telemetryUrl, telemetryToken); -const eventManager = new EventManager(); const threadManager = new ThreadManager(); const voiceChannelManager = new VoiceChannelManager(); const appClient = createDiscordClient(); +const eventManager = new EventManager(appClient); const lockedUsers = new Set(); loginClient(appClient, botToken).then(); diff --git a/src/interactions/button-handlers.ts b/src/interactions/button-handlers.ts index 259ab9a..9c1c1c2 100644 --- a/src/interactions/button-handlers.ts +++ b/src/interactions/button-handlers.ts @@ -1,22 +1,16 @@ -import { - type ButtonInteraction, - type Client, - EmbedBuilder, - type GuildMember, - type TextChannel, +import type { + ButtonInteraction, + Client, + GuildMember, + TextChannel, } from 'discord.js'; import { - COLORS, ERROR_MESSAGES, - FIELD_NAMES, MAX_PARTICIPANTS, - STATUS_MESSAGES, - TIMINGS, WEAPON_ROLES, } from '../constants.js'; import { cleanupEvent, - createEventStartTimeout, promoteNextFromQueue, startEvent, } from '../event/event-lifecycle.js'; @@ -24,11 +18,6 @@ import type { EventManager, ParticipantMap } from '../event/event-manager.js'; import type { ThreadManager } from '../managers/thread-manager.js'; import type { VoiceChannelManager } from '../managers/voice-channel-manager.js'; import type { TelemetryService } from '../telemetry/telemetry.js'; -import { - updateEmbedField, - updateParticipantFields, - updateQueueField, -} from '../utils/embed-utils.js'; import { ErrorSeverity, handleError } from '../utils/error-handler.js'; import { getExcaliburRankOfUser, @@ -81,11 +70,7 @@ export async function handleSignUpButton( ), }); - await eventManager.removeUserFromAllQueues( - userId, - interaction.client, - telemetry, - ); + await eventManager.removeUserFromAllQueues(userId, telemetry); const matchId = eventManager.getMatchId(messageId); telemetry?.trackUserSignUp({ @@ -214,13 +199,8 @@ export async function handleCancelButton( eventManager.setProcessing(messageId, 'cancelling'); try { - const embed = EmbedBuilder.from(interaction.message.embeds[0]).setColor( - COLORS.CANCELLED, - ); - - updateEmbedField(embed, FIELD_NAMES.STATUS, STATUS_MESSAGES.CANCELLED); - - await interaction.editReply({ embeds: [embed], components: [] }); + eventManager.setTerminalState(messageId, 'cancelled'); + await eventManager.queueUpdate(messageId, true); const matchId = eventManager.getMatchId(messageId); telemetry?.trackEventCancelled({ @@ -353,13 +333,8 @@ export async function handleFinishButton( eventManager.setProcessing(messageId, 'finishing'); try { - const embed = EmbedBuilder.from(interaction.message.embeds[0]).setColor( - COLORS.FINISHED, - ); - - updateEmbedField(embed, FIELD_NAMES.STATUS, STATUS_MESSAGES.FINISHED); - - await interaction.editReply({ embeds: [embed], components: [] }); + eventManager.setTerminalState(messageId, 'finished'); + await eventManager.queueUpdate(messageId, true); const matchId = eventManager.getMatchId(messageId); telemetry?.trackEventFinished({ @@ -472,17 +447,7 @@ export async function handleDropOutButton( ); } - const embed = EmbedBuilder.from(interaction.message.embeds[0]); - const updatedParticipantMap = eventManager.getParticipants(messageId); - - if (updatedParticipantMap) { - updateParticipantFields(embed, updatedParticipantMap); - } - - const queue = eventManager.getQueue(messageId); - updateQueueField(embed, queue); - - await interaction.editReply({ embeds: [embed] }); + eventManager.queueUpdate(messageId); const matchId = eventManager.getMatchId(messageId); telemetry?.trackUserDropOut({ @@ -551,7 +516,7 @@ export async function handleDropInButton( ), }); - await eventManager.removeUserFromAllQueues(userId, appClient, telemetry); + await eventManager.removeUserFromAllQueues(userId, telemetry); if (participantMap.size === MAX_PARTICIPANTS) { await eventManager.deleteRepingMessageIfExists(messageId, appClient); @@ -577,9 +542,7 @@ export async function handleDropInButton( ); } - const embed = EmbedBuilder.from(interaction.message.embeds[0]); - updateParticipantFields(embed, participantMap); - await interaction.editReply({ embeds: [embed] }); + eventManager.queueUpdate(messageId); const matchId = eventManager.getMatchId(messageId); telemetry?.trackUserDropIn({ @@ -614,10 +577,7 @@ async function updateParticipantEmbed( voiceChannelManager: VoiceChannelManager, telemetry?: TelemetryService, ) { - const embed = EmbedBuilder.from(interaction.message.embeds[0]); - - updateParticipantFields(embed, participantMap); - + const messageId = interaction.message.id; const timeElapsed = Date.now() - timerData.startTime; const timeIsUpOrNotSet = !timerData.duration || timeElapsed >= timerData.duration; @@ -627,11 +587,11 @@ async function updateParticipantEmbed( timeIsUpOrNotSet && !timerData.hasStarted ) { - await interaction.editReply({ embeds: [embed] }); - createEventStartTimeout( + startEvent( interaction.message, - TIMINGS.EVENT_START_DELAY_MINUTES, + participantMap, eventManager, + interaction.client, threadManager, voiceChannelManager, telemetry, @@ -639,7 +599,7 @@ async function updateParticipantEmbed( return; } - await interaction.editReply({ embeds: [embed] }); + eventManager.queueUpdate(messageId); } export async function handleJoinQueueButton( @@ -683,12 +643,7 @@ export async function handleJoinQueueButton( eventManager.addToQueue(messageId, userId); - const embed = EmbedBuilder.from(interaction.message.embeds[0]); - const queue = eventManager.getQueue(messageId); - - updateQueueField(embed, queue); - - await interaction.editReply({ embeds: [embed] }); + eventManager.queueUpdate(messageId); const matchId = eventManager.getMatchId(messageId); telemetry?.trackUserJoinedQueue({ @@ -739,12 +694,7 @@ export async function handleLeaveQueueButton( eventManager.removeFromQueue(messageId, userId); - const embed = EmbedBuilder.from(interaction.message.embeds[0]); - const queue = eventManager.getQueue(messageId); - - updateQueueField(embed, queue); - - await interaction.editReply({ embeds: [embed] }); + eventManager.queueUpdate(messageId); const matchId = eventManager.getMatchId(messageId); telemetry?.trackUserLeftQueue({ diff --git a/src/interactions/menu-handlers.ts b/src/interactions/menu-handlers.ts index f2d5e48..743ec86 100644 --- a/src/interactions/menu-handlers.ts +++ b/src/interactions/menu-handlers.ts @@ -1,8 +1,6 @@ import type { GuildMember, StringSelectMenuInteraction } from 'discord.js'; -import { EmbedBuilder } from 'discord.js'; import { ERROR_MESSAGES } from '../constants.js'; import type { EventManager } from '../event/event-manager.js'; -import { updateParticipantFields } from '../utils/embed-utils.js'; import { ErrorSeverity, handleError } from '../utils/error-handler.js'; import { getExcaliburRankOfUser, @@ -50,11 +48,7 @@ export async function handleRoleSelection( if (!timerData) return; - const embed = EmbedBuilder.from(interaction.message.embeds[0]); - - updateParticipantFields(embed, participantMap); - - await interaction.editReply({ embeds: [embed] }); + eventManager.queueUpdate(messageId); } catch (error) { handleError({ reason: 'Error handling role selection', diff --git a/src/tests/commands/create-command.test.ts b/src/tests/commands/create-command.test.ts index f4506e6..813f901 100644 --- a/src/tests/commands/create-command.test.ts +++ b/src/tests/commands/create-command.test.ts @@ -76,6 +76,7 @@ describe('create-command', () => { setTimer: vi.fn(), setParticipants: vi.fn(), setGuildId: vi.fn(), + setMessageData: vi.fn(), getParticipants: vi.fn(() => new Map()), removeUserFromAllQueues: vi.fn().mockResolvedValue(undefined), }; diff --git a/src/tests/commands/kick-command.test.ts b/src/tests/commands/kick-command.test.ts index 43ecdf6..09be624 100644 --- a/src/tests/commands/kick-command.test.ts +++ b/src/tests/commands/kick-command.test.ts @@ -89,6 +89,7 @@ describe('kick-command', () => { getMatchId: vi.fn(() => 'match123'), removeParticipant: vi.fn(), getQueue: vi.fn(() => []), + queueUpdate: vi.fn(), }; } diff --git a/src/tests/event/event-lifecycle.test.ts b/src/tests/event/event-lifecycle.test.ts index 00ef8c5..cbe777c 100644 --- a/src/tests/event/event-lifecycle.test.ts +++ b/src/tests/event/event-lifecycle.test.ts @@ -91,6 +91,7 @@ describe('event-lifecycle', () => { deleteRepingMessageIfExists: vi.fn().mockResolvedValue(undefined), clearAllEventData: vi.fn(), deleteProcessingStates: vi.fn(), + queueUpdate: vi.fn(), }; } @@ -407,7 +408,7 @@ describe('event-lifecycle', () => { ); expect(mockEventManager.setTimeout).toHaveBeenCalled(); - expect(mockMessage.edit).toHaveBeenCalled(); + expect(mockEventManager.queueUpdate).toHaveBeenCalledWith('message123'); }); it('should clear existing timeout', async () => { diff --git a/src/tests/event/event-manager.test.ts b/src/tests/event/event-manager.test.ts index 46bcee5..1cc9abe 100644 --- a/src/tests/event/event-manager.test.ts +++ b/src/tests/event/event-manager.test.ts @@ -22,9 +22,14 @@ vi.mock('../../utils/error-handler.js', () => ({ describe('EventManager', () => { let eventManager: EventManager; + let mockClient: Client; beforeEach(() => { - eventManager = new EventManager(); + mockClient = { + channels: { fetch: vi.fn() }, + users: { cache: new Map() }, + } as unknown as Client; + eventManager = new EventManager(mockClient); vi.clearAllMocks(); }); @@ -399,33 +404,21 @@ describe('EventManager', () => { }); it('should remove user from all event queues', async () => { - const client = { - channels: { - fetch: vi.fn(), - }, - } as unknown as Client; - eventManager.addToQueue('event1', 'user1'); eventManager.addToQueue('event1', 'user2'); eventManager.addToQueue('event2', 'user1'); eventManager.addToQueue('event2', 'user3'); - await eventManager.removeUserFromAllQueues('user1', client); + await eventManager.removeUserFromAllQueues('user1'); expect(eventManager.getQueue('event1')).toEqual(['user2']); expect(eventManager.getQueue('event2')).toEqual(['user3']); }); it('should handle removeUserFromAllQueues when user not in any queue', async () => { - const client = { - channels: { - fetch: vi.fn(), - }, - } as unknown as Client; - eventManager.addToQueue('event1', 'user1'); - await eventManager.removeUserFromAllQueues('user2', client); + await eventManager.removeUserFromAllQueues('user2'); expect(eventManager.getQueue('event1')).toEqual(['user1']); }); diff --git a/src/tests/interactions/button-handlers.test.ts b/src/tests/interactions/button-handlers.test.ts index c2020be..eca54bd 100644 --- a/src/tests/interactions/button-handlers.test.ts +++ b/src/tests/interactions/button-handlers.test.ts @@ -95,6 +95,8 @@ describe('button-handlers', () => { deleteTimeout: vi.fn(), removeUserFromAllQueues: vi.fn().mockResolvedValue(undefined), getQueue: vi.fn(() => []), + setTerminalState: vi.fn(), + queueUpdate: vi.fn(), }; } @@ -284,6 +286,14 @@ describe('button-handlers', () => { 'message123', 'cancelling', ); + expect(mockEventManager.setTerminalState).toHaveBeenCalledWith( + 'message123', + 'cancelled', + ); + expect(mockEventManager.queueUpdate).toHaveBeenCalledWith( + 'message123', + true, + ); expect(cleanupEvent).toHaveBeenCalled(); expect(mockEventManager.clearProcessing).toHaveBeenCalledWith( 'message123', @@ -403,6 +413,14 @@ describe('button-handlers', () => { mockTelemetry as never, ); + expect(mockEventManager.setTerminalState).toHaveBeenCalledWith( + 'message123', + 'finished', + ); + expect(mockEventManager.queueUpdate).toHaveBeenCalledWith( + 'message123', + true, + ); expect(cleanupEvent).toHaveBeenCalled(); expect(mockTelemetry.trackEventFinished).toHaveBeenCalled(); }); diff --git a/src/tests/interactions/menu-handlers.test.ts b/src/tests/interactions/menu-handlers.test.ts index a6fadc8..ad05cce 100644 --- a/src/tests/interactions/menu-handlers.test.ts +++ b/src/tests/interactions/menu-handlers.test.ts @@ -54,6 +54,7 @@ describe('menu-handlers', () => { hasStarted: false, })), addParticipant: vi.fn(), + queueUpdate: vi.fn(), }; } @@ -77,7 +78,7 @@ describe('menu-handlers', () => { role: 'Tank', }), ); - expect(mockInteraction.editReply).toHaveBeenCalled(); + expect(mockEventManager.queueUpdate).toHaveBeenCalledWith('message123'); }); it('should reject if user not signed up', async () => { diff --git a/src/tests/utils/embed-utils.test.ts b/src/tests/utils/embed-utils.test.ts index 21b6c6e..aa7fc3b 100644 --- a/src/tests/utils/embed-utils.test.ts +++ b/src/tests/utils/embed-utils.test.ts @@ -1,108 +1,23 @@ -import { EmbedBuilder } from 'discord.js'; import { describe, expect, it } from 'vitest'; import { COLORS, FIELD_NAMES, - MAX_PARTICIPANTS, PARTICIPANT_FIELD_NAME, - STATUS_MESSAGES, WEAPON_ROLES, } from '../../constants.js'; -import type { ParticipantMap } from '../../event/event-manager.js'; import { createEventButtons, createEventEmbed, createEventStartedButtons, createRoleSelectMenu, - updateEmbedField, - updateEmbedFieldByMatch, - updateParticipantFields, } from '../../utils/embed-utils.js'; describe('embed-utils', () => { - describe('updateEmbedField', () => { - it('should update an existing field value', () => { - const embed = new EmbedBuilder().addFields({ - name: 'Test Field', - value: 'Old Value', - }); - - updateEmbedField(embed, 'Test Field', 'New Value'); - - const fields = embed.data.fields || []; - const field = fields.find((f) => f.name === 'Test Field'); - expect(field?.value).toBe('New Value'); - }); - - it('should not modify non-matching fields', () => { - const embed = new EmbedBuilder().addFields( - { name: 'Field 1', value: 'Value 1' }, - { name: 'Field 2', value: 'Value 2' }, - ); - - updateEmbedField(embed, 'Field 1', 'Updated'); - - const fields = embed.data.fields || []; - expect(fields.find((f) => f.name === 'Field 1')?.value).toBe('Updated'); - expect(fields.find((f) => f.name === 'Field 2')?.value).toBe('Value 2'); - }); - - it('should handle missing field gracefully', () => { - const embed = new EmbedBuilder().addFields({ - name: 'Existing', - value: 'Value', - }); - - updateEmbedField(embed, 'Non-Existent', 'New'); - - const fields = embed.data.fields || []; - expect(fields.length).toBe(1); - expect(fields[0].value).toBe('Value'); - }); - }); - - describe('updateEmbedFieldByMatch', () => { - it('should update field matching partial name', () => { - const embed = new EmbedBuilder().addFields({ - name: 'Participants (2)', - value: 'List', - }); - - updateEmbedFieldByMatch( - embed, - 'Participants', - 'Participants (3)', - '- @user1\n- @user2\n- @user3', - ); - - const fields = embed.data.fields || []; - const field = fields[0]; - expect(field.name).toBe('Participants (3)'); - expect(field.value).toBe('- @user1\n- @user2\n- @user3'); - }); - - it('should not update non-matching fields', () => { - const embed = new EmbedBuilder().addFields( - { name: 'Status', value: 'Open' }, - { name: 'Role', value: 'None' }, - ); - - updateEmbedFieldByMatch( - embed, - 'Participants', - 'Participants (1)', - '@user', - ); - - const fields = embed.data.fields || []; - expect(fields[0].name).toBe('Status'); - expect(fields[1].name).toBe('Role'); - }); - }); - describe('createEventEmbed', () => { it('should create embed with all required fields for casual event', () => { const embed = createEventEmbed( + null, + null, 'TestUser', 'https://example.com/avatar.png', '123456789', @@ -124,6 +39,8 @@ describe('embed-utils', () => { it('should create embed with all required fields for competitive event', () => { const embed = createEventEmbed( + null, + null, 'TestUser', 'https://example.com/avatar.png', '123456789', @@ -135,6 +52,8 @@ describe('embed-utils', () => { it('should include timer information when provided', () => { const embed = createEventEmbed( + null, + null, 'TestUser', 'https://example.com/avatar.png', '123456789', @@ -149,6 +68,8 @@ describe('embed-utils', () => { it('should include description when info is provided', () => { const embed = createEventEmbed( + null, + null, 'TestUser', 'https://example.com/avatar.png', '123456789', @@ -162,6 +83,8 @@ describe('embed-utils', () => { it('should set default weapon role', () => { const embed = createEventEmbed( + null, + null, 'TestUser', 'https://example.com/avatar.png', '123456789', @@ -254,73 +177,4 @@ describe('embed-utils', () => { expect(selectMenu.data.placeholder).toBeTruthy(); }); }); - - describe('updateParticipantFields', () => { - it('should update participant count and list', () => { - const embed = new EmbedBuilder().addFields( - { name: PARTICIPANT_FIELD_NAME(1), value: '- <@user1>' }, - { name: FIELD_NAMES.ROLE, value: '- None' }, - { name: FIELD_NAMES.STATUS, value: STATUS_MESSAGES.OPEN }, - ); - - const participantMap: ParticipantMap = new Map([ - ['user1', { userId: 'user1', role: 'Slayer', rank: null }], - ['user2', { userId: 'user2', role: 'Support', rank: null }], - ]); - - updateParticipantFields(embed, participantMap); - - const fields = embed.data.fields || []; - const participantField = fields.find((f) => - f.name.includes('Participants'), - ); - expect(participantField?.name).toBe(PARTICIPANT_FIELD_NAME(2)); - expect(participantField?.value).toContain('user1'); - expect(participantField?.value).toContain('user2'); - }); - - it('should update status to ready when full', () => { - const embed = new EmbedBuilder().addFields( - { name: PARTICIPANT_FIELD_NAME(1), value: '- <@user1>' }, - { name: FIELD_NAMES.ROLE, value: '- None' }, - { name: FIELD_NAMES.STATUS, value: STATUS_MESSAGES.OPEN }, - ); - - const participantMap: ParticipantMap = new Map(); - for (let i = 0; i < MAX_PARTICIPANTS; i++) { - participantMap.set(`user${i}`, { - userId: `user${i}`, - role: 'None', - rank: null, - }); - } - - updateParticipantFields(embed, participantMap); - - const fields = embed.data.fields || []; - const statusField = fields.find((f) => f.name === FIELD_NAMES.STATUS); - - expect([STATUS_MESSAGES.READY]).toContain(statusField?.value); - }); - - it('should update roles alongside participants', () => { - const embed = new EmbedBuilder().addFields( - { name: PARTICIPANT_FIELD_NAME(1), value: '- <@user1>' }, - { name: FIELD_NAMES.ROLE, value: '- None' }, - { name: FIELD_NAMES.STATUS, value: STATUS_MESSAGES.OPEN }, - ); - - const participantMap: ParticipantMap = new Map([ - ['user1', { userId: 'user1', role: '🔪 Slayer', rank: null }], - ['user2', { userId: 'user2', role: '🛡️ Support', rank: null }], - ]); - - updateParticipantFields(embed, participantMap); - - const fields = embed.data.fields || []; - const roleField = fields.find((f) => f.name === FIELD_NAMES.ROLE); - expect(roleField?.value).toContain('🔪 Slayer'); - expect(roleField?.value).toContain('🛡️ Support'); - }); - }); }); diff --git a/src/tests/utils/helpers.test.ts b/src/tests/utils/helpers.test.ts index 2cdb3f7..da824f9 100644 --- a/src/tests/utils/helpers.test.ts +++ b/src/tests/utils/helpers.test.ts @@ -19,6 +19,7 @@ import { EventManager } from '../../event/event-manager.js'; import { botHasPermission, checkProcessingStates, + getEmoteForRank, getExcaliburRankOfUser, getPingsForServer, isUserAdmin, @@ -416,9 +417,14 @@ describe('helpers', () => { describe('checkProcessingStates', () => { let eventManager: EventManager; + let mockClient: Client; beforeEach(() => { - eventManager = new EventManager(); + mockClient = { + channels: { fetch: vi.fn() }, + users: { cache: new Map() }, + } as unknown as Client; + eventManager = new EventManager(mockClient); }); it('should return true when event is starting', async () => { @@ -472,4 +478,103 @@ describe('helpers', () => { expect(interaction.reply).toHaveBeenCalled(); }); }); + + describe('getEmoteForRank', () => { + it('should return empty string for non-Excalibur guild', () => { + const result = getEmoteForRank('differentGuildId', '1'); + + expect(result).toBe(''); + }); + + it('should return empty string when guildId is null', () => { + const result = getEmoteForRank(null, '1'); + + expect(result).toBe(''); + }); + + it('should return empty string when guildId is undefined', () => { + const result = getEmoteForRank(undefined, '1'); + + expect(result).toBe(''); + }); + + it('should return default emote when rankId is null', () => { + const result = getEmoteForRank(EXCALIBUR_GUILD_ID, null); + + expect(result).toBe('⚫ '); + }); + + it('should return default emote when rankId is invalid', () => { + const result = getEmoteForRank(EXCALIBUR_GUILD_ID, 'invalidRank'); + + expect(result).toBe('⚫ '); + }); + + it('should return correct emote for rank 1 (Grandmaster)', () => { + const result = getEmoteForRank(EXCALIBUR_GUILD_ID, '1'); + + expect(result).toBe( + `<:${EXCALIBUR_RANKS['1'].emoteName}:${EXCALIBUR_RANKS['1'].emoteId}> `, + ); + expect(result).toContain('Ex8s1_grandmaster'); + }); + + it('should return correct emote for rank 2 (Legend)', () => { + const result = getEmoteForRank(EXCALIBUR_GUILD_ID, '2'); + + expect(result).toBe( + `<:${EXCALIBUR_RANKS['2'].emoteName}:${EXCALIBUR_RANKS['2'].emoteId}> `, + ); + expect(result).toContain('Ex8s2_legend'); + }); + + it('should return correct emote for rank 3 (Ascendant)', () => { + const result = getEmoteForRank(EXCALIBUR_GUILD_ID, '3'); + + expect(result).toBe( + `<:${EXCALIBUR_RANKS['3'].emoteName}:${EXCALIBUR_RANKS['3'].emoteId}> `, + ); + expect(result).toContain('Ex8s3_ascendant'); + }); + + it('should return correct emote for rank 4 (Elite)', () => { + const result = getEmoteForRank(EXCALIBUR_GUILD_ID, '4'); + + expect(result).toBe( + `<:${EXCALIBUR_RANKS['4'].emoteName}:${EXCALIBUR_RANKS['4'].emoteId}> `, + ); + expect(result).toContain('Ex8s4_elite'); + }); + + it('should return correct emote for rank 5 (Knight)', () => { + const result = getEmoteForRank(EXCALIBUR_GUILD_ID, '5'); + + expect(result).toBe( + `<:${EXCALIBUR_RANKS['5'].emoteName}:${EXCALIBUR_RANKS['5'].emoteId}> `, + ); + expect(result).toContain('Ex8s5_knight'); + }); + + it('should return correct emote for rank 6 (Squire)', () => { + const result = getEmoteForRank(EXCALIBUR_GUILD_ID, '6'); + + expect(result).toBe( + `<:${EXCALIBUR_RANKS['6'].emoteName}:${EXCALIBUR_RANKS['6'].emoteId}> `, + ); + expect(result).toContain('Ex8s6_novice'); + }); + + it('should include trailing space in emote string', () => { + const result = getEmoteForRank(EXCALIBUR_GUILD_ID, '1'); + + expect(result).toMatch(/ $/); + }); + + it('should include trailing space in default emote', () => { + const result = getEmoteForRank(EXCALIBUR_GUILD_ID, null); + + expect(result).toBe('⚫ '); + expect(result).toMatch(/ $/); + }); + }); }); diff --git a/src/utils/embed-utils.ts b/src/utils/embed-utils.ts index 480b954..40989c4 100644 --- a/src/utils/embed-utils.ts +++ b/src/utils/embed-utils.ts @@ -9,7 +9,6 @@ import { import { COLORS, FIELD_NAMES, - MAX_PARTICIPANTS, PARTICIPANT_FIELD_NAME, START_MESSAGES, STATUS_MESSAGES, @@ -17,37 +16,11 @@ import { TITLES, WEAPON_ROLES, } from '../constants.js'; -import type { ParticipantMap } from '../event/event-manager.js'; - -export function updateEmbedField( - embed: EmbedBuilder, - fieldName: string, - newValue: string, -) { - const fields = embed.data.fields || []; - const field = fields.find((f) => f.name === fieldName); - if (field) { - field.value = newValue; - } - embed.setFields(fields); -} - -export function updateEmbedFieldByMatch( - embed: EmbedBuilder, - partialName: string, - newName: string, - newValue: string, -) { - const fields = embed.data.fields || []; - const field = fields.find((f) => f.name.includes(partialName)); - if (field) { - field.name = newName; - field.value = newValue; - } - embed.setFields(fields); -} +import { getEmoteForRank } from './helpers.js'; export function createEventEmbed( + guildId: string | null | undefined, + rankId: string | null, username: string, avatarUrl: string, userId: string, @@ -59,7 +32,7 @@ export function createEventEmbed( const embedFields = [ { name: PARTICIPANT_FIELD_NAME(1), - value: `- <@${userId}>`, + value: `- ${getEmoteForRank(guildId, rankId)}<@${userId}>`, inline: true, }, { @@ -167,58 +140,3 @@ export function createRoleSelectMenu() { return new ActionRowBuilder().addComponents(select); } - -export function updateParticipantFields( - embed: EmbedBuilder, - participantMap: ParticipantMap, -) { - updateEmbedFieldByMatch( - embed, - FIELD_NAMES.PARTICIPANTS, - PARTICIPANT_FIELD_NAME(participantMap.size), - Array.from(participantMap.values()) - .map((p) => `- <@${p.userId}>`) - .join('\n'), - ); - - updateEmbedField( - embed, - FIELD_NAMES.ROLE, - Array.from(participantMap.values()) - .map((p) => `- ${p.role || 'None'}`) - .join('\n'), - ); - - const status = - participantMap.size === MAX_PARTICIPANTS - ? STATUS_MESSAGES.READY - : STATUS_MESSAGES.OPEN; - updateEmbedField(embed, FIELD_NAMES.STATUS, status); -} - -export function updateQueueField(embed: EmbedBuilder, queue: string[]) { - const fields = embed.data.fields || []; - const queueField = fields.find((f) => f.name === FIELD_NAMES.QUEUE); - - if (queue.length > 0) { - const queueValue = queue.map((userId) => `- <@${userId}>`).join('\n'); - - if (queueField) { - queueField.value = queueValue; - } else { - fields.push({ - name: FIELD_NAMES.QUEUE, - value: queueValue, - inline: false, - }); - } - } else { - const index = fields.findIndex((f) => f.name === FIELD_NAMES.QUEUE); - - if (index !== -1) { - fields.splice(index, 1); - } - } - - embed.setFields(fields); -} diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index c275eb8..3026893 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -84,6 +84,18 @@ export function getExcaliburRankOfUser( return null; } +export function getEmoteForRank( + guildId: string | null | undefined, + rankId: string | null, +) { + if (guildId !== EXCALIBUR_GUILD_ID) return ''; + if (!rankId || !(rankId in EXCALIBUR_RANKS)) return '⚫ '; + + const rank = EXCALIBUR_RANKS[rankId as keyof typeof EXCALIBUR_RANKS]; + + return `<:${rank.emoteName}:${rank.emoteId}> `; +} + export async function safeReplyToInteraction( interaction: RepliableInteraction, content: string,