import { IssueStatus, IssueTypeName } from '@server/constants/issue'; import { MediaStatus } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; import type { NotificationAgentPushover } from '@server/lib/settings'; import { getSettings, NotificationAgentKey } from '@server/lib/settings'; import logger from '@server/logger'; import axios from 'axios'; import { hasNotificationType, Notification, shouldSendAdminNotification, } from '..'; import type { NotificationAgent, NotificationPayload } from './agent'; import { BaseAgent } from './agent'; interface PushoverImagePayload { attachment_base64: string; attachment_type: string; } interface PushoverPayload extends PushoverImagePayload { token: string; user: string; title: string; message: string; url: string; url_title: string; priority: number; html: number; } class PushoverAgent extends BaseAgent implements NotificationAgent { protected getSettings(): NotificationAgentPushover { if (this.settings) { return this.settings; } const settings = getSettings(); return settings.notifications.agents.pushover; } public shouldSend(): boolean { return true; } private async getImagePayload( imageUrl: string ): Promise> { try { const response = await axios.get(imageUrl, { responseType: 'arraybuffer', }); const base64 = Buffer.from(response.data, 'binary').toString('base64'); const contentType = ( response.headers['Content-Type'] || response.headers['content-type'] )?.toString(); return { attachment_base64: base64, attachment_type: contentType, }; } catch (e) { logger.error('Error getting image payload', { label: 'Notifications', errorMessage: e.message, response: e.response?.data, }); return {}; } } private async getNotificationPayload( type: Notification, payload: NotificationPayload ): Promise> { const { applicationUrl, applicationTitle } = getSettings().main; const title = payload.event ?? payload.subject; let message = payload.event ? `${payload.subject}` : ''; let priority = 0; if (payload.message) { message += `${message ? '\n' : ''}${payload.message}`; } if (payload.request) { message += `\n\nRequested By: ${payload.request.requestedBy.displayName}`; let status = ''; switch (type) { case Notification.MEDIA_AUTO_REQUESTED: status = payload.media?.status === MediaStatus.PENDING ? 'Pending Approval' : 'Processing'; break; case Notification.MEDIA_PENDING: status = 'Pending Approval'; break; case Notification.MEDIA_APPROVED: case Notification.MEDIA_AUTO_APPROVED: status = 'Processing'; break; case Notification.MEDIA_AVAILABLE: status = 'Available'; break; case Notification.MEDIA_DECLINED: status = 'Declined'; priority = 1; break; case Notification.MEDIA_FAILED: status = 'Failed'; priority = 1; break; } if (status) { message += `\nRequest Status: ${status}`; } } else if (payload.comment) { message += `\n\nComment from ${payload.comment.user.displayName}: ${payload.comment.message}`; } else if (payload.issue) { message += `\n\nReported By: ${payload.issue.createdBy.displayName}`; message += `\nIssue Type: ${ IssueTypeName[payload.issue.issueType] }`; message += `\nIssue Status: ${ payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved' }`; if (type === Notification.ISSUE_CREATED) { priority = 1; } } for (const extra of payload.extra ?? []) { message += `\n${extra.name}: ${extra.value}`; } const url = applicationUrl ? payload.issue ? `${applicationUrl}/issues/${payload.issue.id}` : payload.media ? `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}` : undefined : undefined; const url_title = url ? `View ${payload.issue ? 'Issue' : 'Media'} in ${applicationTitle}` : undefined; let attachment_base64; let attachment_type; if (payload.image) { const imagePayload = await this.getImagePayload(payload.image); if (imagePayload.attachment_base64 && imagePayload.attachment_type) { attachment_base64 = imagePayload.attachment_base64; attachment_type = imagePayload.attachment_type; } } return { title, message, url, url_title, priority, html: 1, attachment_base64, attachment_type, }; } public async send( type: Notification, payload: NotificationPayload ): Promise { const settings = this.getSettings(); const endpoint = 'https://api.pushover.net/1/messages.json'; const notificationPayload = await this.getNotificationPayload( type, payload ); // Send system notification if ( payload.notifySystem && hasNotificationType(type, settings.types ?? 0) && settings.enabled && settings.options.accessToken && settings.options.userToken ) { logger.debug('Sending Pushover notification', { label: 'Notifications', type: Notification[type], subject: payload.subject, }); try { await axios.post(endpoint, { ...notificationPayload, token: settings.options.accessToken, user: settings.options.userToken, sound: settings.options.sound, } as PushoverPayload); } catch (e) { logger.error('Error sending Pushover notification', { label: 'Notifications', type: Notification[type], subject: payload.subject, errorMessage: e.message, response: e.response?.data, }); return false; } } if (payload.notifyUser) { if ( payload.notifyUser.settings?.hasNotificationType( NotificationAgentKey.PUSHOVER, type ) && payload.notifyUser.settings.pushoverApplicationToken && payload.notifyUser.settings.pushoverUserKey && (payload.notifyUser.settings.pushoverApplicationToken !== settings.options.accessToken || payload.notifyUser.settings.pushoverUserKey !== settings.options.userToken) ) { logger.debug('Sending Pushover notification', { label: 'Notifications', recipient: payload.notifyUser.displayName, type: Notification[type], subject: payload.subject, }); try { await axios.post(endpoint, { ...notificationPayload, token: payload.notifyUser.settings.pushoverApplicationToken, user: payload.notifyUser.settings.pushoverUserKey, sound: payload.notifyUser.settings.pushoverSound, } as PushoverPayload); } catch (e) { logger.error('Error sending Pushover notification', { label: 'Notifications', recipient: payload.notifyUser.displayName, type: Notification[type], subject: payload.subject, errorMessage: e.message, response: e.response?.data, }); return false; } } } if (payload.notifyAdmin) { const userRepository = getRepository(User); const users = await userRepository.find(); await Promise.all( users .filter( (user) => user.settings?.hasNotificationType( NotificationAgentKey.PUSHOVER, type ) && shouldSendAdminNotification(type, user, payload) ) .map(async (user) => { if ( user.settings?.pushoverApplicationToken && user.settings?.pushoverUserKey && user.settings.pushoverApplicationToken !== settings.options.accessToken && user.settings.pushoverUserKey !== settings.options.userToken ) { logger.debug('Sending Pushover notification', { label: 'Notifications', recipient: user.displayName, type: Notification[type], subject: payload.subject, }); try { await axios.post(endpoint, { ...notificationPayload, token: user.settings.pushoverApplicationToken, user: user.settings.pushoverUserKey, } as PushoverPayload); } catch (e) { logger.error('Error sending Pushover notification', { label: 'Notifications', recipient: user.displayName, type: Notification[type], subject: payload.subject, errorMessage: e.message, response: e.response?.data, }); return false; } } }) ); } return true; } } export default PushoverAgent;