* chore(deps): update dependencies and fix security vulnerabilities Update TypeScript 4.9 → 5.4. Update Zod 3 → 4. Update nodemailer 6 → 7. Update @typescript-eslint packages to v7. Update xml2js, undici, lodash, axios, swr, winston- Add pnpm.overrides for transitive dependency vulnerabilities * chore: fix import ordering for TypeScript 5.4 compatibility prettier-plugin-organize-imports behaves differently with TypeScript 5.4 vs 4.9, causing CI formatting checks to fail. This reformats imports to match the ordering expected by the plugin with the upgraded TS version.
333 lines
9.7 KiB
TypeScript
333 lines
9.7 KiB
TypeScript
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 { NotificationAgentKey, getSettings } from '@server/lib/settings';
|
|
import logger from '@server/logger';
|
|
import axios from 'axios';
|
|
import {
|
|
Notification,
|
|
hasNotificationType,
|
|
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<NotificationAgentPushover>
|
|
implements NotificationAgent
|
|
{
|
|
protected getSettings(): NotificationAgentPushover {
|
|
if (this.settings) {
|
|
return this.settings;
|
|
}
|
|
|
|
const settings = getSettings();
|
|
|
|
return settings.notifications.agents.pushover;
|
|
}
|
|
|
|
public shouldSend(): boolean {
|
|
const settings = this.getSettings();
|
|
|
|
if (
|
|
settings.enabled &&
|
|
settings.options.accessToken &&
|
|
settings.options.userToken
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async getImagePayload(
|
|
imageUrl: string
|
|
): Promise<Partial<PushoverImagePayload>> {
|
|
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<Partial<PushoverPayload>> {
|
|
const settings = getSettings();
|
|
const { applicationUrl, applicationTitle } = settings.main;
|
|
const { embedPoster } = settings.notifications.agents.pushover;
|
|
|
|
const title = payload.event ?? payload.subject;
|
|
let message = payload.event ? `<b>${payload.subject}</b>` : '';
|
|
let priority = 0;
|
|
|
|
if (payload.message) {
|
|
message += `<small>${message ? '\n' : ''}${payload.message}</small>`;
|
|
}
|
|
|
|
if (payload.request) {
|
|
message += `<small>\n\n<b>Requested By:</b> ${payload.request.requestedBy.displayName}</small>`;
|
|
|
|
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 += `<small>\n<b>Request Status:</b> ${status}</small>`;
|
|
}
|
|
} else if (payload.comment) {
|
|
message += `<small>\n\n<b>Comment from ${payload.comment.user.displayName}:</b> ${payload.comment.message}</small>`;
|
|
} else if (payload.issue) {
|
|
message += `<small>\n\n<b>Reported By:</b> ${payload.issue.createdBy.displayName}</small>`;
|
|
message += `<small>\n<b>Issue Type:</b> ${
|
|
IssueTypeName[payload.issue.issueType]
|
|
}</small>`;
|
|
message += `<small>\n<b>Issue Status:</b> ${
|
|
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
|
|
}</small>`;
|
|
|
|
if (type === Notification.ISSUE_CREATED) {
|
|
priority = 1;
|
|
}
|
|
}
|
|
|
|
for (const extra of payload.extra ?? []) {
|
|
message += `<small>\n<b>${extra.name}:</b> ${extra.value}</small>`;
|
|
}
|
|
|
|
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 (embedPoster && 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<boolean> {
|
|
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;
|