You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
overseerr/server/lib/notifications/agents/webpush.ts

249 lines
7.3 KiB

import { getRepository } from 'typeorm';
import webpush from 'web-push';
import { Notification, shouldSendAdminNotification } from '..';
import { IssueType, IssueTypeName } from '../../../constants/issue';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
import logger from '../../../logger';
import type { NotificationAgentConfig } from '../../settings';
import { getSettings, NotificationAgentKey } from '../../settings';
import type { NotificationAgent, NotificationPayload } from './agent';
import { BaseAgent } from './agent';
interface PushNotificationPayload {
notificationType: string;
subject: string;
message?: string;
image?: string;
actionUrl?: string;
actionUrlTitle?: string;
requestId?: number;
}
class WebPushAgent
extends BaseAgent<NotificationAgentConfig>
implements NotificationAgent
{
protected getSettings(): NotificationAgentConfig {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.webpush;
}
private getNotificationPayload(
type: Notification,
payload: NotificationPayload
): PushNotificationPayload {
const mediaType = payload.media
? payload.media.mediaType === MediaType.MOVIE
? 'movie'
: 'series'
: undefined;
const is4k = payload.request?.is4k;
const issueType = payload.issue
? payload.issue.issueType !== IssueType.OTHER
? `${IssueTypeName[payload.issue.issueType].toLowerCase()} issue`
: 'issue'
: undefined;
let message: string | undefined;
switch (type) {
case Notification.TEST_NOTIFICATION:
message = payload.message;
break;
case Notification.MEDIA_APPROVED:
message = `Your ${
is4k ? '4K ' : ''
}${mediaType} request has been approved.`;
break;
case Notification.MEDIA_AUTO_APPROVED:
message = `Automatically approved a new ${
is4k ? '4K ' : ''
}${mediaType} request from ${
payload.request?.requestedBy.displayName
}.`;
break;
case Notification.MEDIA_AVAILABLE:
message = `Your ${
is4k ? '4K ' : ''
}${mediaType} request is now available!`;
break;
case Notification.MEDIA_DECLINED:
message = `Your ${is4k ? '4K ' : ''}${mediaType} request was declined.`;
break;
case Notification.MEDIA_FAILED:
message = `Failed to process ${is4k ? '4K ' : ''}${mediaType} request.`;
break;
case Notification.MEDIA_PENDING:
message = `Approval required for a new ${
is4k ? '4K ' : ''
}${mediaType} request from ${
payload.request?.requestedBy.displayName
}.`;
break;
case Notification.ISSUE_CREATED:
message = `A new ${issueType} was reported by ${payload.issue?.createdBy.displayName}.`;
break;
case Notification.ISSUE_COMMENT:
message = `${payload.comment?.user.displayName} commented on the ${issueType}.`;
break;
case Notification.ISSUE_RESOLVED:
message = `The ${issueType} was marked as resolved by ${payload.issue?.modifiedBy?.displayName}!`;
break;
case Notification.ISSUE_REOPENED:
message = `The ${issueType} was reopened by ${payload.issue?.modifiedBy?.displayName}.`;
break;
default:
return {
notificationType: Notification[type],
subject: 'Unknown',
};
}
const actionUrl = payload.issue
? `/issues/${payload.issue.id}`
: payload.media
? `/${payload.media.mediaType}/${payload.media.tmdbId}`
: undefined;
const actionUrlTitle = actionUrl
? `View ${payload.issue ? 'Issue' : 'Media'}`
: undefined;
return {
notificationType: Notification[type],
subject: payload.subject,
message,
image: payload.image,
requestId: payload.request?.id,
actionUrl,
actionUrlTitle,
};
}
public shouldSend(): boolean {
if (this.getSettings().enabled) {
return true;
}
return false;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const userRepository = getRepository(User);
const userPushSubRepository = getRepository(UserPushSubscription);
const settings = getSettings();
const pushSubs: UserPushSubscription[] = [];
const mainUser = await userRepository.findOne({ where: { id: 1 } });
if (
payload.notifyUser &&
// Check if user has webpush notifications enabled and fallback to true if undefined
// since web push should default to true
(payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.WEBPUSH,
type
) ??
true)
) {
const notifySubs = await userPushSubRepository.find({
where: { user: payload.notifyUser.id },
});
pushSubs.push(...notifySubs);
}
if (payload.notifyAdmin) {
const users = await userRepository.find();
const manageUsers = users.filter(
(user) =>
// Check if user has webpush notifications enabled and fallback to true if undefined
// since web push should default to true
(user.settings?.hasNotificationType(
NotificationAgentKey.WEBPUSH,
type
) ??
true) &&
shouldSendAdminNotification(type, user, payload)
);
const allSubs = await userPushSubRepository
.createQueryBuilder('pushSub')
.leftJoinAndSelect('pushSub.user', 'user')
.where('pushSub.userId IN (:users)', {
users: manageUsers.map((user) => user.id),
})
.getMany();
pushSubs.push(...allSubs);
}
if (mainUser && pushSubs.length > 0) {
webpush.setVapidDetails(
`mailto:${mainUser.email}`,
settings.vapidPublic,
settings.vapidPrivate
);
const notificationPayload = Buffer.from(
JSON.stringify(this.getNotificationPayload(type, payload)),
'utf-8'
);
await Promise.all(
pushSubs.map(async (sub) => {
logger.debug('Sending web push notification', {
label: 'Notifications',
recipient: sub.user.displayName,
type: Notification[type],
subject: payload.subject,
});
try {
await webpush.sendNotification(
{
endpoint: sub.endpoint,
keys: {
auth: sub.auth,
p256dh: sub.p256dh,
},
},
notificationPayload
);
} catch (e) {
logger.error(
'Error sending web push notification; removing subscription',
{
label: 'Notifications',
recipient: sub.user.displayName,
type: Notification[type],
subject: payload.subject,
errorMessage: e.message,
}
);
// Failed to send notification so we need to remove the subscription
userPushSubRepository.remove(sub);
}
})
);
}
return true;
}
}
export default WebPushAgent;