import config from '../config.ts'; import { Bot } from '../deps.ts'; import { LfgEmbedIndexes } from './buttons/eventUtils.ts'; import { getEventMemberCount, getGuildName, getLfgMembers } from './buttons/live-event/utils.ts'; import { failColor, infoColor1, sendDirectMessage, warnColor } from './commandUtils.ts'; import { reportSlashName } from './commands/slashCommandNames.ts'; import { dbClient } from './db/client.ts'; import { queries } from './db/common.ts'; import { ActiveEvent } from './types/commandTypes.ts'; import utils from './utils.ts'; const notifyStepName = 'notify'; const lockStepName = 'lock'; const deleteStepName = 'delete'; export const tenMinutes = 10 * 60 * 1000; // Join strings with english in mind const joinWithAnd = (words: string[]) => { if (words.length === 0) { return ''; } else if (words.length === 1) { return words[0]; } else if (words.length === 2) { return words.join(' and '); } else { return words.slice(0, -1).join(', ') + ', and ' + words.slice(-1); } }; // Log the failure in a loud sense const loudLogFailure = async (bot: Bot, event: ActiveEvent, stepName: string, secondFailure = false) => { const guildName = await getGuildName(bot, event.guildId); const eventUrl = utils.idsToMessageUrl({ guildId: event.guildId, channelId: event.channelId, messageId: event.messageId, }); // Try to DM owner if this is the second time it has failed let dmSuccess = false; if (secondFailure) { const msg = await sendDirectMessage(bot, event.ownerId, { embeds: [{ color: failColor, title: `Attention: Failed to ${stepName} one of your events`, description: `${config.name} tried twice to find [this event](${eventUrl}) in ${guildName}, and could not either time. Since ${config.name} has failed twice, ${config.name} has now removed [this event](${eventUrl}) from its list of active events. [This event](${eventUrl}) was scheduled to start at . The message containing this event may have been deleted by a moderator or administrator in ${guildName}. If [the event](${eventUrl}) still exists when you click on the link above, please \`/${reportSlashName}\` this issue to the developers with the full error code below.`, fields: [{ name: 'Error Code:', value: `\`loudLog@${event.guildId}|${event.channelId}|${event.messageId}|${event.ownerId}|${event.eventTime.getTime()}|${event.notifiedFlag}|${event.lockedFlag}@\``, }], }], }).catch((e: Error) => utils.commonLoggers.messageSendError('notificationSystem.ts@loudLog', 'send DM fail', e)); dmSuccess = Boolean(msg); } // Log this to bot's log channel const dmSuccessStr = dmSuccess ? 'SUCCESSFULLY' : 'NOT'; bot.helpers.sendMessage(config.logChannel, { content: secondFailure ? `Hey <@${config.owner}>, something may have gone wrong. The owner of this event was ${dmSuccessStr} notified.` : undefined, embeds: [{ color: secondFailure ? failColor : warnColor, title: `Failed to ${stepName} an event in ${guildName}. This is the ${secondFailure ? 'second' : 'first'} attempt.`, description: `${config.name} failed to ${stepName} [this event](${eventUrl}).\n\nDebug Data:`, fields: [{ name: 'Guild ID:', value: `${event.guildId}`, inline: true, }, { name: 'Channel ID:', value: `${event.channelId}`, inline: true, }, { name: 'Message ID:', value: `${event.messageId}`, inline: true, }, { name: 'Owner ID:', value: `${event.ownerId}`, inline: true, }, { name: 'Event Time:', value: ``, inline: true, }, { name: 'Notified Flag:', value: `${event.notifiedFlag}`, inline: true, }, { name: 'Locked Flag:', value: `${event.lockedFlag}`, inline: true, }], }], }).catch((e: Error) => utils.commonLoggers.messageSendError('notificationSystem.ts@loudLog', 'send log message', e)); }; // Notifies all members of the event and edits the event message export const notifyEventMembers = async (bot: Bot, event: ActiveEvent, secondTry = false): Promise => { const eventMessage = await bot.helpers.getMessage(event.channelId, event.messageId).catch((e: Error) => utils.commonLoggers.messageGetError('notificationSystem.ts@notify', 'get event', e)); if (eventMessage?.embeds[0].fields) { const activityName = `\`${eventMessage.embeds[0].fields[LfgEmbedIndexes.Activity].name} ${eventMessage.embeds[0].fields[LfgEmbedIndexes.Activity].value}\``; const members = getLfgMembers(eventMessage.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].value); const memberMentionString = joinWithAnd(members.map((member) => `<@${member.id}>`)); const guildName = await getGuildName(bot, event.guildId); // Edit event in guild await bot.helpers.editMessage(event.channelId, event.messageId, { content: `Attention ${memberMentionString}, your ${activityName} starts in less than 10 minutes.`, }).catch((e: Error) => utils.commonLoggers.messageEditError('notificationSystem.ts@notify', 'event edit fail', e)); // Send the notifications to the members members.forEach(async (member) => { await sendDirectMessage(bot, member.id, { embeds: [{ color: infoColor1, title: `Hello ${member.name}! Your activity in ${guildName} starts in less than 10 minutes.`, description: 'Please start grouping up with the other members of this activity:', }, eventMessage.embeds[0]], }).catch((e: Error) => utils.commonLoggers.messageSendError('notificationSystem.ts@notify', 'send DM fail', e)); }); // Update DB to indicate notifications have been sent out dbClient.execute(queries.updateEventFlags(1, 0), [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@notifySuccess', 'update event in', e)); return true; } else { if (!secondTry) loudLogFailure(bot, event, notifyStepName).catch((e) => utils.commonLoggers.messageSendError('notificationSystem.ts@notify', 'loudLog Failed', e)); // Update DB to indicate notifications have not been sent out dbClient.execute(queries.updateEventFlags(-1, 0), [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@notifyFail', 'update event in', e)); return false; } }; // Locks the event message and notifies alternates if necessary export const lockEvent = async (bot: Bot, event: ActiveEvent, secondTry = false): Promise => { const eventMessage = await bot.helpers.getMessage(event.channelId, event.messageId).catch((e: Error) => utils.commonLoggers.messageGetError('notificationSystem.ts@lock', 'get event', e)); if (eventMessage?.embeds[0].fields) { const [currentMemberCount, maxMemberCount] = getEventMemberCount(eventMessage.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].name); const alternates = getLfgMembers(eventMessage.embeds[0].fields[LfgEmbedIndexes.AlternateMembers].value); const memberMentionString = joinWithAnd(alternates.map((member) => `<@${member.id}>`)); // See if event was filled or not, and if not notify alternates const alternatesNeeded = alternates.length && currentMemberCount < maxMemberCount; if (alternatesNeeded) { const activityName = `\`${eventMessage.embeds[0].fields[LfgEmbedIndexes.Activity].name} ${eventMessage.embeds[0].fields[LfgEmbedIndexes.Activity].value}\``; const guildName = await getGuildName(bot, event.guildId); const peopleShort = maxMemberCount - currentMemberCount; // Send the notifications to the members alternates.forEach(async (member) => { await sendDirectMessage(bot, member.id, { embeds: [{ color: infoColor1, title: `Hello ${member.name}! An activity in ${guildName} may need your help.`, description: `The ${activityName} in ${guildName} that you marked yourself as an alternate for may be \`${peopleShort}\` ${ peopleShort === 1 ? 'person' : 'people' } short. If you are available, please join up with them.`, }, eventMessage.embeds[0]], }).catch((e: Error) => utils.commonLoggers.messageSendError('notificationSystem.ts@lock', 'send DM fail', e)); }); } // Edit event in guild await bot.helpers.editMessage( event.channelId, event.messageId, alternatesNeeded ? { content: `${eventMessage.content}\n\nAttention ${memberMentionString}, this activity is \`${maxMemberCount - currentMemberCount}\` people short. Please join up if you are available.`, components: [], } : { components: [], }, ).catch((e: Error) => utils.commonLoggers.messageEditError('notificationSystem.ts@lock', 'event edit fail', e)); // Update DB to indicate event has been locked dbClient.execute(queries.updateEventFlags(1, 1), [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@lockSuccess', 'update event in', e)); return true; } else { if (!secondTry) loudLogFailure(bot, event, lockStepName).catch((e) => utils.commonLoggers.messageSendError('notificationSystem.ts@lock', 'loudLog Failed', e)); // Update DB to indicate event has not been locked dbClient.execute(queries.updateEventFlags(1, -1), [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@lockFail', 'update event in', e)); return false; } }; // Notifies all members of the event and edits the event message export const deleteEvent = async (bot: Bot, event: ActiveEvent, secondTry = false): Promise => { const eventMessage = await bot.helpers.getMessage(event.channelId, event.messageId).catch((e: Error) => utils.commonLoggers.messageGetError('notificationSystem.ts@delete', 'get event', e)); if (eventMessage?.embeds[0].fields) { // Delete event in guild await bot.helpers.deleteMessage(event.channelId, event.messageId, 'Cleaning up activity that has started').catch((e: Error) => utils.commonLoggers.messageDeleteError('notificationSystem.ts@delete', 'event delete fail', e) ); // Remove event from DB dbClient.execute(queries.deleteEvent, [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@deleteSuccess', 'delete event from', e)); return true; } else { if (!secondTry) loudLogFailure(bot, event, deleteStepName).catch((e) => utils.commonLoggers.messageSendError('notificationSystem.ts@delete', 'loudLog Failed', e)); // Update DB to indicate delete failed dbClient.execute(queries.updateEventFlags(-1, -1), [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@deleteFail', 'update event in', e)); return false; } }; // Handles trying again once and cleaning up events that died export const handleFailures = async (bot: Bot, event: ActiveEvent) => { let rerunSuccess: boolean; let stepName: string; // Retry the step that failed if (event.notifiedFlag === -1 && event.lockedFlag === -1) { rerunSuccess = await deleteEvent(bot, event, true); stepName = deleteStepName; } else if (event.lockedFlag === -1) { rerunSuccess = await lockEvent(bot, event, true); stepName = lockStepName; } else if (event.notifiedFlag === -1) { rerunSuccess = await notifyEventMembers(bot, event, true); stepName = notifyStepName; } else { // Should never get here as this func should only be called when event has one of the flags as -1 // Set flag to true since it already succeeded? rerunSuccess = true; stepName = ''; } if (!rerunSuccess) { // Failed at completing a step! Event may have been deleted? loudLogFailure(bot, event, stepName, true).catch((e) => utils.commonLoggers.messageSendError('notificationSystem.ts@rerun', 'loudLog Failed', e)); dbClient.execute(queries.deleteEvent, [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@handleFailures', 'delete event from', e)); } };