import axios from 'axios'; import { getRepository } from 'typeorm'; import { hasNotificationType, Notification } from '..'; import { IssueStatus, IssueTypeNames } from '../../../constants/issue'; import { MediaType } from '../../../constants/media'; import { User } from '../../../entity/User'; import logger from '../../../logger'; import { Permission } from '../../permissions'; import { getSettings, NotificationAgentDiscord, NotificationAgentKey, } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; enum EmbedColors { DEFAULT = 0, AQUA = 1752220, GREEN = 3066993, BLUE = 3447003, PURPLE = 10181046, GOLD = 15844367, ORANGE = 15105570, RED = 15158332, GREY = 9807270, DARKER_GREY = 8359053, NAVY = 3426654, DARK_AQUA = 1146986, DARK_GREEN = 2067276, DARK_BLUE = 2123412, DARK_PURPLE = 7419530, DARK_GOLD = 12745742, DARK_ORANGE = 11027200, DARK_RED = 10038562, DARK_GREY = 9936031, LIGHT_GREY = 12370112, DARK_NAVY = 2899536, LUMINOUS_VIVID_PINK = 16580705, DARK_VIVID_PINK = 12320855, } interface DiscordImageEmbed { url?: string; proxy_url?: string; height?: number; width?: number; } interface Field { name: string; value: string; inline?: boolean; } interface DiscordRichEmbed { title?: string; type?: 'rich'; // Always rich for webhooks description?: string; url?: string; timestamp?: string; color?: number; footer?: { text: string; icon_url?: string; proxy_icon_url?: string; }; image?: DiscordImageEmbed; thumbnail?: DiscordImageEmbed; provider?: { name?: string; url?: string; }; author?: { name?: string; url?: string; icon_url?: string; proxy_icon_url?: string; }; fields?: Field[]; } interface DiscordWebhookPayload { embeds: DiscordRichEmbed[]; username?: string; avatar_url?: string; tts: boolean; content?: string; allowed_mentions?: { parse?: ('users' | 'roles' | 'everyone')[]; roles?: string[]; users?: string[]; }; } class DiscordAgent extends BaseAgent implements NotificationAgent { protected getSettings(): NotificationAgentDiscord { if (this.settings) { return this.settings; } const settings = getSettings(); return settings.notifications.agents.discord; } public buildEmbed( type: Notification, payload: NotificationPayload ): DiscordRichEmbed { const settings = getSettings(); let color = EmbedColors.DARK_PURPLE; const fields: Field[] = []; if (payload.request) { fields.push({ name: 'Requested By', value: payload.request.requestedBy.displayName, inline: true, }); } // If payload has an issue attached, push issue specific fields if (payload.issue) { fields.push( { name: 'Created By', value: payload.issue.createdBy.displayName, inline: true, }, { name: 'Issue Type', value: IssueTypeNames[payload.issue.issueType], inline: true, }, { name: 'Issue Status', value: payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved', inline: true, } ); if (payload.issue.media.mediaType === MediaType.TV) { fields.push({ name: 'Affected Season', value: payload.issue.problemSeason > 0 ? `Season ${payload.issue.problemSeason}` : 'All Seasons', }); if (payload.issue.problemSeason > 0) { fields.push({ name: 'Affected Episode', value: payload.issue.problemEpisode > 0 ? `Episode ${payload.issue.problemEpisode}` : 'All Episodes', }); } } } switch (type) { case Notification.MEDIA_PENDING: color = EmbedColors.ORANGE; fields.push({ name: 'Status', value: 'Pending Approval', inline: true, }); break; case Notification.MEDIA_APPROVED: case Notification.MEDIA_AUTO_APPROVED: color = EmbedColors.PURPLE; fields.push({ name: 'Status', value: 'Processing', inline: true, }); break; case Notification.MEDIA_AVAILABLE: color = EmbedColors.GREEN; fields.push({ name: 'Status', value: 'Available', inline: true, }); break; case Notification.MEDIA_DECLINED: color = EmbedColors.RED; fields.push({ name: 'Status', value: 'Declined', inline: true, }); break; case Notification.MEDIA_FAILED: color = EmbedColors.RED; fields.push({ name: 'Status', value: 'Failed', inline: true, }); break; case Notification.ISSUE_CREATED: case Notification.ISSUE_COMMENT: case Notification.ISSUE_RESOLVED: color = EmbedColors.ORANGE; if (payload.issue && payload.issue.status === IssueStatus.RESOLVED) { color = EmbedColors.GREEN; } break; } const url = settings.main.applicationUrl && payload.media ? `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}` : undefined; return { title: payload.subject, url, description: payload.message, color, timestamp: new Date().toISOString(), author: { name: settings.main.applicationTitle, url: settings.main.applicationUrl, }, fields: [ ...fields, // If we have extra data, map it to fields for discord notifications ...(payload.extra ?? []).map((extra) => ({ name: extra.name, value: extra.value, })), ], thumbnail: { url: payload.image, }, }; } public shouldSend(): boolean { const settings = this.getSettings(); if (settings.enabled && settings.options.webhookUrl) { return true; } return false; } public async send( type: Notification, payload: NotificationPayload ): Promise { const settings = this.getSettings(); if (!hasNotificationType(type, settings.types ?? 0)) { return true; } logger.debug('Sending Discord notification', { label: 'Notifications', type: Notification[type], subject: payload.subject, }); let content = undefined; try { if (payload.notifyUser) { // Mention user who submitted the request if ( payload.notifyUser.settings?.hasNotificationType( NotificationAgentKey.DISCORD, type ) && payload.notifyUser.settings?.discordId ) { content = `<@${payload.notifyUser.settings.discordId}>`; } } else { // Mention all users with the Manage Requests permission const userRepository = getRepository(User); const users = await userRepository.find(); content = users .filter( (user) => user.hasPermission(Permission.MANAGE_REQUESTS) && user.settings?.hasNotificationType( NotificationAgentKey.DISCORD, type ) && user.settings?.discordId && // Check if it's the user's own auto-approved request (type !== Notification.MEDIA_AUTO_APPROVED || user.id !== payload.request?.requestedBy.id) ) .map((user) => `<@${user.settings?.discordId}>`) .join(' '); } await axios.post(settings.options.webhookUrl, { username: settings.options.botUsername, avatar_url: settings.options.botAvatarUrl, embeds: [this.buildEmbed(type, payload)], content, } as DiscordWebhookPayload); return true; } catch (e) { logger.error('Error sending Discord notification', { label: 'Notifications', mentions: content, type: Notification[type], subject: payload.subject, errorMessage: e.message, response: e.response?.data, }); return false; } } } export default DiscordAgent;