feat: PWA Support (#1488)

This commit is contained in:
sct
2021-04-25 20:44:12 +09:00
committed by GitHub
parent e6e5ad221a
commit 28830d4ef8
88 changed files with 2022 additions and 650 deletions

View File

@@ -29,6 +29,7 @@ import { getSettings } from '../lib/settings';
import logger from '../logger';
import { MediaRequest } from './MediaRequest';
import SeasonRequest from './SeasonRequest';
import { UserPushSubscription } from './UserPushSubscription';
import { UserSettings } from './UserSettings';
@Entity()
@@ -105,6 +106,9 @@ export class User {
})
public settings?: UserSettings;
@OneToMany(() => UserPushSubscription, (pushSub) => pushSub.user)
public pushSubscriptions: UserPushSubscription[];
@CreateDateColumn()
public createdAt: Date;

View File

@@ -0,0 +1,27 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { User } from './User';
@Entity()
export class UserPushSubscription {
@PrimaryGeneratedColumn()
public id: number;
@ManyToOne(() => User, (user) => user.pushSubscriptions, {
eager: true,
onDelete: 'CASCADE',
})
public user: User;
@Column()
public endpoint: string;
@Column()
public p256dh: string;
@Column({ unique: true })
public auth: string;
constructor(init?: Partial<UserPushSubscription>) {
Object.assign(this, init);
}
}

View File

@@ -5,12 +5,15 @@ import {
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import {
hasNotificationAgentEnabled,
NotificationAgentType,
} from '../lib/notifications/agenttypes';
import { NotificationAgentTypes } from '../interfaces/api/userSettingsInterfaces';
import { hasNotificationType, Notification } from '../lib/notifications';
import { NotificationAgentKey } from '../lib/settings';
import { User } from './User';
export const ALL_NOTIFICATIONS = Object.values(Notification)
.filter((v) => !isNaN(Number(v)))
.reduce((a, v) => a + Number(v), 0);
@Entity()
export class UserSettings {
constructor(init?: Partial<UserSettings>) {
@@ -24,15 +27,15 @@ export class UserSettings {
@JoinColumn()
public user: User;
@Column({ default: 'en' })
public locale?: string;
@Column({ nullable: true })
public region?: string;
@Column({ nullable: true })
public originalLanguage?: string;
@Column({ type: 'integer', default: NotificationAgentType.EMAIL })
public notificationAgents = NotificationAgentType.EMAIL;
@Column({ nullable: true })
public pgpKey?: string;
@@ -45,7 +48,63 @@ export class UserSettings {
@Column({ nullable: true })
public telegramSendSilently?: boolean;
public hasNotificationAgentEnabled(agent: NotificationAgentType): boolean {
return !!hasNotificationAgentEnabled(agent, this.notificationAgents);
@Column({
type: 'text',
nullable: true,
transformer: {
from: (value: string | null): Partial<NotificationAgentTypes> => {
const defaultTypes = {
email: ALL_NOTIFICATIONS,
discord: 0,
pushbullet: 0,
pushover: 0,
slack: 0,
telegram: 0,
webhook: 0,
webpush: ALL_NOTIFICATIONS,
};
if (!value) {
return defaultTypes;
}
const values = JSON.parse(value) as Partial<NotificationAgentTypes>;
// Something with the migration to this field has caused some issue where
// the value pre-populates with just a raw "2"? Here we check if that's the case
// and return the default notification types if so
if (typeof values !== 'object') {
return defaultTypes;
}
if (values.email == null) {
values.email = ALL_NOTIFICATIONS;
}
if (values.webpush == null) {
values.webpush = ALL_NOTIFICATIONS;
}
return values;
},
to: (value: Partial<NotificationAgentTypes>): string => {
const allowedKeys = Object.values(NotificationAgentKey);
// Remove any unknown notification agent keys before saving to db
(Object.keys(value) as (keyof NotificationAgentTypes)[]).forEach(
(key) => {
if (!allowedKeys.includes(key)) {
delete value[key];
}
}
);
return JSON.stringify(value);
},
},
})
public notificationTypes: Partial<NotificationAgentTypes>;
public hasNotificationType(key: NotificationAgentKey, type: Notification) {
return hasNotificationType(type, this.notificationTypes[key] ?? 0);
}
}

View File

@@ -1,30 +1,31 @@
import express, { Request, Response, NextFunction } from 'express';
import next from 'next';
import path from 'path';
import { createConnection, getRepository } from 'typeorm';
import routes from './routes';
import { getClientIp } from '@supercharge/request-ip';
import bodyParser from 'body-parser';
import { TypeormStore } from 'connect-typeorm/out';
import cookieParser from 'cookie-parser';
import csurf from 'csurf';
import session, { Store } from 'express-session';
import { TypeormStore } from 'connect-typeorm/out';
import YAML from 'yamljs';
import swaggerUi from 'swagger-ui-express';
import express, { NextFunction, Request, Response } from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
import session, { Store } from 'express-session';
import next from 'next';
import path from 'path';
import swaggerUi from 'swagger-ui-express';
import { createConnection, getRepository } from 'typeorm';
import YAML from 'yamljs';
import { Session } from './entity/Session';
import { getSettings } from './lib/settings';
import logger from './logger';
import { startJobs } from './job/schedule';
import notificationManager from './lib/notifications';
import DiscordAgent from './lib/notifications/agents/discord';
import EmailAgent from './lib/notifications/agents/email';
import TelegramAgent from './lib/notifications/agents/telegram';
import { getAppVersion } from './utils/appVersion';
import SlackAgent from './lib/notifications/agents/slack';
import PushoverAgent from './lib/notifications/agents/pushover';
import WebhookAgent from './lib/notifications/agents/webhook';
import { getClientIp } from '@supercharge/request-ip';
import PushbulletAgent from './lib/notifications/agents/pushbullet';
import PushoverAgent from './lib/notifications/agents/pushover';
import SlackAgent from './lib/notifications/agents/slack';
import TelegramAgent from './lib/notifications/agents/telegram';
import WebhookAgent from './lib/notifications/agents/webhook';
import WebPushAgent from './lib/notifications/agents/webpush';
import { getSettings } from './lib/settings';
import logger from './logger';
import routes from './routes';
import { getAppVersion } from './utils/appVersion';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
@@ -57,6 +58,7 @@ app
new SlackAgent(),
new TelegramAgent(),
new WebhookAgent(),
new WebPushAgent(),
]);
// Start Jobs

View File

@@ -30,6 +30,8 @@ export interface PublicSettingsResponse {
originalLanguage: string;
partialRequestsEnabled: boolean;
cacheImages: boolean;
vapidPublic: string;
enablePushRegistration: boolean;
}
export interface CacheItem {

View File

@@ -1,5 +1,8 @@
import { NotificationAgentKey } from '../../lib/settings';
export interface UserSettingsGeneralResponse {
username?: string;
locale?: string;
region?: string;
originalLanguage?: string;
movieQuotaLimit?: number;
@@ -12,8 +15,8 @@ export interface UserSettingsGeneralResponse {
globalTvQuotaDays?: number;
}
export type NotificationAgentTypes = Record<NotificationAgentKey, number>;
export interface UserSettingsNotificationsResponse {
notificationAgents: number;
emailEnabled?: boolean;
pgpKey?: string;
discordEnabled?: boolean;
@@ -22,4 +25,6 @@ export interface UserSettingsNotificationsResponse {
telegramBotUsername?: string;
telegramChatId?: string;
telegramSendSilently?: boolean;
webPushEnabled?: boolean;
notificationTypes: Partial<NotificationAgentTypes>;
}

View File

@@ -4,8 +4,11 @@ import { hasNotificationType, Notification } from '..';
import { User } from '../../../entity/User';
import logger from '../../../logger';
import { Permission } from '../../permissions';
import { getSettings, NotificationAgentDiscord } from '../../settings';
import { NotificationAgentType } from '../agenttypes';
import {
getSettings,
NotificationAgentDiscord,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
enum EmbedColors {
@@ -227,8 +230,9 @@ class DiscordAgent
if (payload.notifyUser) {
// Mention user who submitted the request
if (
payload.notifyUser.settings?.hasNotificationAgentEnabled(
NotificationAgentType.DISCORD
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
payload.notifyUser.settings?.discordId
) {
@@ -243,8 +247,9 @@ class DiscordAgent
.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
user.settings?.hasNotificationAgentEnabled(
NotificationAgentType.DISCORD
user.settings?.hasNotificationType(
NotificationAgentKey.DISCORD,
type
) &&
user.settings?.discordId
)

View File

@@ -7,8 +7,11 @@ import { User } from '../../../entity/User';
import logger from '../../../logger';
import PreparedEmail from '../../email';
import { Permission } from '../../permissions';
import { getSettings, NotificationAgentEmail } from '../../settings';
import { NotificationAgentType } from '../agenttypes';
import {
getSettings,
NotificationAgentEmail,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
class EmailAgent
@@ -152,9 +155,13 @@ class EmailAgent
// Send notification to the user who submitted the request
if (
!payload.notifyUser.settings ||
payload.notifyUser.settings.hasNotificationAgentEnabled(
NotificationAgentType.EMAIL
)
// Check if user has email notifications enabled and fallback to true if undefined
// since email should default to true
(payload.notifyUser.settings.hasNotificationType(
NotificationAgentKey.EMAIL,
type
) ??
true)
) {
logger.debug('Sending email notification', {
label: 'Notifications',
@@ -194,9 +201,13 @@ class EmailAgent
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
(!user.settings ||
user.settings.hasNotificationAgentEnabled(
NotificationAgentType.EMAIL
))
// Check if user has email notifications enabled and fallback to true if undefined
// since email should default to true
(user.settings.hasNotificationType(
NotificationAgentKey.EMAIL,
type
) ??
true))
)
.map(async (user) => {
logger.debug('Sending email notification', {

View File

@@ -2,8 +2,11 @@ import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
import logger from '../../../logger';
import { getSettings, NotificationAgentTelegram } from '../../settings';
import { NotificationAgentType } from '../agenttypes';
import {
getSettings,
NotificationAgentKey,
NotificationAgentTelegram,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface TelegramMessagePayload {
@@ -198,8 +201,9 @@ class TelegramAgent
if (
payload.notifyUser &&
payload.notifyUser.settings?.hasNotificationAgentEnabled(
NotificationAgentType.TELEGRAM
payload.notifyUser.settings?.hasNotificationType(
NotificationAgentKey.TELEGRAM,
type
) &&
payload.notifyUser.settings?.telegramChatId &&
payload.notifyUser.settings?.telegramChatId !==

View File

@@ -0,0 +1,234 @@
import { getRepository } from 'typeorm';
import webpush from 'web-push';
import { hasNotificationType, Notification } from '..';
import { MediaType } from '../../../constants/media';
import { User } from '../../../entity/User';
import { UserPushSubscription } from '../../../entity/UserPushSubscription';
import logger from '../../../logger';
import { Permission } from '../../permissions';
import {
getSettings,
NotificationAgentConfig,
NotificationAgentKey,
} from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface PushNotificationPayload {
notificationType: string;
mediaType?: 'movie' | 'tv';
tmdbId?: number;
subject: string;
message?: string;
image?: string;
actionUrl?: 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 {
switch (type) {
case Notification.TEST_NOTIFICATION:
return {
notificationType: Notification[type],
subject: payload.subject,
message: payload.message,
};
case Notification.MEDIA_APPROVED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request has been approved.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_AUTO_APPROVED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Automatically approved a new ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request from ${payload.request?.requestedBy.displayName}.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_AVAILABLE:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request is now available!`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_DECLINED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Your ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request was declined.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_FAILED:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Failed to process ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
case Notification.MEDIA_PENDING:
return {
notificationType: Notification[type],
subject: payload.subject,
message: `Approval required for new ${
payload.media?.mediaType === MediaType.MOVIE ? 'movie' : 'series'
} request from ${payload.request?.requestedBy.displayName}.`,
image: payload.image,
mediaType: payload.media?.mediaType,
tmdbId: payload.media?.tmdbId,
requestId: payload.request?.id,
actionUrl: `/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
};
}
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending web push notification', {
label: 'Notifications',
type: Notification[type],
subject: payload.subject,
});
const userRepository = getRepository(User);
const userPushSubRepository = getRepository(UserPushSubscription);
const settings = getSettings();
let 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 = notifySubs;
} else if (!payload.notifyUser) {
const users = await userRepository.find();
const manageUsers = users.filter(
(user) =>
user.hasPermission(Permission.MANAGE_REQUESTS) &&
// 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)
);
const allSubs = await userPushSubRepository
.createQueryBuilder('pushSub')
.where('pushSub.userId IN (:users)', {
users: manageUsers.map((user) => user.id),
})
.getMany();
pushSubs = allSubs;
}
if (mainUser && pushSubs.length > 0) {
webpush.setVapidDetails(
`mailto:${mainUser.email}`,
settings.vapidPublic,
settings.vapidPrivate
);
Promise.all(
pushSubs.map(async (sub) => {
try {
await webpush.sendNotification(
{
endpoint: sub.endpoint,
keys: {
auth: sub.auth,
p256dh: sub.p256dh,
},
},
Buffer.from(
JSON.stringify(this.getNotificationPayload(type, payload)),
'utf-8'
)
);
} catch (e) {
// Failed to send notification so we need to remove the subscription
userPushSubRepository.remove(sub);
}
})
);
}
return true;
}
}
export default WebPushAgent;

View File

@@ -1,16 +0,0 @@
export enum NotificationAgentType {
NONE = 0,
EMAIL = 2,
DISCORD = 4,
TELEGRAM = 8,
PUSHOVER = 16,
PUSHBULLET = 32,
SLACK = 64,
}
export const hasNotificationAgentEnabled = (
agent: NotificationAgentType,
value: number
): boolean => {
return !!(value & agent);
};

View File

@@ -2,6 +2,7 @@ import fs from 'fs';
import { merge } from 'lodash';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import webpush from 'web-push';
import { Permission } from './permissions';
export interface Library {
@@ -101,6 +102,8 @@ interface FullPublicSettings extends PublicSettings {
originalLanguage: string;
partialRequestsEnabled: boolean;
cacheImages: boolean;
vapidPublic: string;
enablePushRegistration: boolean;
}
export interface NotificationAgentConfig {
@@ -168,6 +171,17 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig {
};
}
export enum NotificationAgentKey {
DISCORD = 'discord',
EMAIL = 'email',
PUSHBULLET = 'pushbullet',
PUSHOVER = 'pushover',
SLACK = 'slack',
TELEGRAM = 'telegram',
WEBHOOK = 'webhook',
WEBPUSH = 'webpush',
}
interface NotificationAgents {
discord: NotificationAgentDiscord;
email: NotificationAgentEmail;
@@ -176,6 +190,7 @@ interface NotificationAgents {
slack: NotificationAgentSlack;
telegram: NotificationAgentTelegram;
webhook: NotificationAgentWebhook;
webpush: NotificationAgentConfig;
}
interface NotificationSettings {
@@ -184,6 +199,8 @@ interface NotificationSettings {
interface AllSettings {
clientId: string;
vapidPublic: string;
vapidPrivate: string;
main: MainSettings;
plex: PlexSettings;
radarr: RadarrSettings[];
@@ -202,6 +219,8 @@ class Settings {
constructor(initialSettings?: AllSettings) {
this.data = {
clientId: uuidv4(),
vapidPrivate: '',
vapidPublic: '',
main: {
apiKey: '',
applicationTitle: 'Overseerr',
@@ -298,6 +317,11 @@ class Settings {
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW10sXG4gICAgXCJ7e3JlcXVlc3R9fVwiOiB7XG4gICAgICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfZW1haWxcIjogXCJ7e3JlcXVlc3RlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV91c2VybmFtZVwiOiBcInt7cmVxdWVzdGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIlxuICAgIH1cbn0i',
},
},
webpush: {
enabled: false,
types: 0,
options: {},
},
},
},
};
@@ -366,6 +390,8 @@ class Settings {
originalLanguage: this.data.main.originalLanguage,
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
cacheImages: this.data.main.cacheImages,
vapidPublic: this.vapidPublic,
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
};
}
@@ -386,6 +412,18 @@ class Settings {
return this.data.clientId;
}
get vapidPublic(): string {
this.generateVapidKeys();
return this.data.vapidPublic;
}
get vapidPrivate(): string {
this.generateVapidKeys();
return this.data.vapidPrivate;
}
public regenerateApiKey(): MainSettings {
this.main.apiKey = this.generateApiKey();
this.save();
@@ -396,6 +434,15 @@ class Settings {
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64');
}
private generateVapidKeys(force = false): void {
if (!this.data.vapidPublic || !this.data.vapidPrivate || force) {
const vapidKeys = webpush.generateVAPIDKeys();
this.data.vapidPrivate = vapidKeys.privateKey;
this.data.vapidPublic = vapidKeys.publicKey;
this.save();
}
}
/**
* Settings Load
*

View File

@@ -28,6 +28,7 @@ export const checkUser: Middleware = async (req, _res, next) => {
if (user) {
req.user = user;
req.locale = user.settings?.locale;
}
}
next();

View File

@@ -0,0 +1,36 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUserPushSubscriptions1618912653565
implements MigrationInterface {
name = 'CreateUserPushSubscriptions1618912653565';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"))`
);
await queryRunner.query(
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId") SELECT "id", "endpoint", "p256dh", "auth", "userId" FROM "user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"))`
);
await queryRunner.query(
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId") SELECT "id", "endpoint", "p256dh", "auth", "userId" FROM "temporary_user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
}
}

View File

@@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserSettingsLocale1619239659754 implements MigrationInterface {
name = 'AddUserSettingsLocale1619239659754';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT ('en'), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
}
}

View File

@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserSettingsNotificationTypes1619339817343
implements MigrationInterface {
name = 'AddUserSettingsNotificationTypes1619339817343';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT ('en'), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" text, "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT ('en'), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "user_settings"`
);
await queryRunner.query(`DROP TABLE "user_settings"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationTypes" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT ('en'), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
await queryRunner.query(
`ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"`
);
await queryRunner.query(
`CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT ('en'), CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale") SELECT "id", "notificationTypes", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey", "locale" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
}
}

View File

@@ -11,7 +11,7 @@ collectionRoutes.get<{ id: string }>('/:id', async (req, res, next) => {
try {
const collection = await tmdb.getCollection({
collectionId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

View File

@@ -1,16 +1,16 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapMovieResult, mapTvResult, mapPersonResult } from '../models/Search';
import Media from '../entity/Media';
import { isMovie, isPerson } from '../utils/typeHelpers';
import { MediaType } from '../constants/media';
import { getSettings } from '../lib/settings';
import { User } from '../entity/User';
import { mapProductionCompany } from '../models/Movie';
import { mapNetwork } from '../models/Tv';
import logger from '../logger';
import { sortBy } from 'lodash';
import TheMovieDb from '../api/themoviedb';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import { User } from '../entity/User';
import { GenreSliderItem } from '../interfaces/api/discoverInterfaces';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import { mapProductionCompany } from '../models/Movie';
import { mapMovieResult, mapPersonResult, mapTvResult } from '../models/Search';
import { mapNetwork } from '../models/Tv';
import { isMovie, isPerson } from '../utils/typeHelpers';
const createTmdbWithRegionLanaguage = (user?: User): TheMovieDb => {
const settings = getSettings();
@@ -42,7 +42,7 @@ discoverRoutes.get('/movies', async (req, res) => {
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: req.query.genre ? Number(req.query.genre) : undefined,
studio: req.query.studio ? Number(req.query.studio) : undefined,
});
@@ -83,7 +83,7 @@ discoverRoutes.get<{ language: string }>(
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
originalLanguage: req.params.language,
});
@@ -115,7 +115,7 @@ discoverRoutes.get<{ genreId: string }>(
const tmdb = createTmdbWithRegionLanaguage(req.user);
const genres = await tmdb.getMovieGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const genre = genres.find(
@@ -128,7 +128,7 @@ discoverRoutes.get<{ genreId: string }>(
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: Number(req.params.genreId),
});
@@ -164,7 +164,7 @@ discoverRoutes.get<{ studioId: string }>(
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
studio: Number(req.params.studioId),
});
@@ -204,7 +204,7 @@ discoverRoutes.get('/movies/upcoming', async (req, res) => {
const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
primaryReleaseDateGte: date,
});
@@ -232,7 +232,7 @@ discoverRoutes.get('/tv', async (req, res) => {
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: req.query.genre ? Number(req.query.genre) : undefined,
network: req.query.network ? Number(req.query.network) : undefined,
});
@@ -273,7 +273,7 @@ discoverRoutes.get<{ language: string }>(
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
originalLanguage: req.params.language,
});
@@ -304,7 +304,7 @@ discoverRoutes.get<{ genreId: string }>(
const tmdb = createTmdbWithRegionLanaguage(req.user);
const genres = await tmdb.getTvGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const genre = genres.find(
@@ -317,7 +317,7 @@ discoverRoutes.get<{ genreId: string }>(
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
genre: Number(req.params.genreId),
});
@@ -352,7 +352,7 @@ discoverRoutes.get<{ networkId: string }>(
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
network: Number(req.params.networkId),
});
@@ -392,7 +392,7 @@ discoverRoutes.get('/tv/upcoming', async (req, res) => {
const data = await tmdb.getDiscoverTv({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
firstAirDateGte: date,
});
@@ -420,7 +420,7 @@ discoverRoutes.get('/trending', async (req, res) => {
const data = await tmdb.getAllTrending({
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@@ -461,7 +461,7 @@ discoverRoutes.get<{ keywordId: string }>(
const data = await tmdb.getMoviesByKeyword({
keywordId: Number(req.params.keywordId),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@@ -494,7 +494,7 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
const mappedGenres: GenreSliderItem[] = [];
const genres = await tmdb.getMovieGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
await Promise.all(
@@ -535,7 +535,7 @@ discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
const mappedGenres: GenreSliderItem[] = [];
const genres = await tmdb.getTvGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
await Promise.all(

View File

@@ -138,7 +138,7 @@ router.get('/genres/movie', isAuthenticated(), async (req, res) => {
const tmdb = new TheMovieDb();
const genres = await tmdb.getMovieGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(genres);
@@ -148,7 +148,7 @@ router.get('/genres/tv', isAuthenticated(), async (req, res) => {
const tmdb = new TheMovieDb();
const genres = await tmdb.getTvGenres({
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(genres);

View File

@@ -1,11 +1,11 @@
import { Router } from 'express';
import RottenTomatoes from '../api/rottentomatoes';
import TheMovieDb from '../api/themoviedb';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import logger from '../logger';
import { mapMovieDetails } from '../models/Movie';
import { mapMovieResult } from '../models/Search';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';
import logger from '../logger';
import { MediaType } from '../constants/media';
const movieRoutes = Router();
@@ -15,7 +15,7 @@ movieRoutes.get('/:id', async (req, res, next) => {
try {
const tmdbMovie = await tmdb.getMovie({
movieId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getMedia(tmdbMovie.id, MediaType.MOVIE);
@@ -36,7 +36,7 @@ movieRoutes.get('/:id/recommendations', async (req, res) => {
const results = await tmdb.getMovieRecommendations({
movieId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@@ -64,7 +64,7 @@ movieRoutes.get('/:id/similar', async (req, res) => {
const results = await tmdb.getMovieSimilar({
movieId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

View File

@@ -16,7 +16,7 @@ personRoutes.get('/:id', async (req, res, next) => {
try {
const person = await tmdb.getPerson({
personId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(mapPersonDetails(person));
} catch (e) {
@@ -30,7 +30,7 @@ personRoutes.get('/:id/combined_credits', async (req, res) => {
const combinedCredits = await tmdb.getPersonCombinedCredits({
personId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const castMedia = await Media.getRelatedMedia(

View File

@@ -1,7 +1,7 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapSearchResults } from '../models/Search';
import Media from '../entity/Media';
import { mapSearchResults } from '../models/Search';
const searchRoutes = Router();
@@ -11,7 +11,7 @@ searchRoutes.get('/', async (req, res) => {
const results = await tmdb.searchMulti({
query: req.query.query as string,
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

View File

@@ -191,7 +191,7 @@ serviceRoutes.get<{ tmdbId: string }>(
try {
const tv = await tmdb.getTvShow({
tvId: Number(req.params.tmdbId),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const response = await sonarr.getSeriesByTitle(tv.name);

View File

@@ -7,6 +7,7 @@ import PushoverAgent from '../../lib/notifications/agents/pushover';
import SlackAgent from '../../lib/notifications/agents/slack';
import TelegramAgent from '../../lib/notifications/agents/telegram';
import WebhookAgent from '../../lib/notifications/agents/webhook';
import WebPushAgent from '../../lib/notifications/agents/webpush';
import { getSettings } from '../../lib/settings';
const notificationRoutes = Router();
@@ -215,6 +216,40 @@ notificationRoutes.post('/email/test', (req, res, next) => {
return res.status(204).send();
});
notificationRoutes.get('/webpush', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.notifications.agents.webpush);
});
notificationRoutes.post('/webpush', (req, res) => {
const settings = getSettings();
settings.notifications.agents.webpush = req.body;
settings.save();
res.status(200).json(settings.notifications.agents.webpush);
});
notificationRoutes.post('/webpush/test', (req, res, next) => {
if (!req.user) {
return next({
status: 500,
message: 'User information missing from request',
});
}
const webpushAgent = new WebPushAgent(req.body);
webpushAgent.send(Notification.TEST_NOTIFICATION, {
notifyUser: req.user,
subject: 'Test Notification',
message:
'This is a test notification! Check check, 1, 2, 3. Are we coming in clear?',
});
return res.status(204).send();
});
notificationRoutes.get('/webhook', (_req, res) => {
const settings = getSettings();

View File

@@ -1,11 +1,11 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapTvDetails, mapSeasonWithEpisodes } from '../models/Tv';
import { mapTvResult } from '../models/Search';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';
import logger from '../logger';
import TheMovieDb from '../api/themoviedb';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import logger from '../logger';
import { mapTvResult } from '../models/Search';
import { mapSeasonWithEpisodes, mapTvDetails } from '../models/Tv';
const tvRoutes = Router();
@@ -14,7 +14,7 @@ tvRoutes.get('/:id', async (req, res, next) => {
try {
const tv = await tmdb.getTvShow({
tvId: Number(req.params.id),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getMedia(tv.id, MediaType.TV);
@@ -35,7 +35,7 @@ tvRoutes.get('/:id/season/:seasonNumber', async (req, res) => {
const season = await tmdb.getTvSeason({
tvId: Number(req.params.id),
seasonNumber: Number(req.params.seasonNumber),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
return res.status(200).json(mapSeasonWithEpisodes(season));
@@ -47,7 +47,7 @@ tvRoutes.get('/:id/recommendations', async (req, res) => {
const results = await tmdb.getTvRecommendations({
tvId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(
@@ -75,7 +75,7 @@ tvRoutes.get('/:id/similar', async (req, res) => {
const results = await tmdb.getTvSimilar({
tvId: Number(req.params.id),
page: Number(req.query.page),
language: req.query.language as string,
language: req.locale ?? (req.query.language as string),
});
const media = await Media.getRelatedMedia(

View File

@@ -5,6 +5,7 @@ import PlexTvAPI from '../../api/plextv';
import { UserType } from '../../constants/user';
import { MediaRequest } from '../../entity/MediaRequest';
import { User } from '../../entity/User';
import { UserPushSubscription } from '../../entity/UserPushSubscription';
import {
QuotaResponse,
UserRequestsResponse,
@@ -127,6 +128,48 @@ router.post(
}
);
router.post<
never,
unknown,
{
endpoint: string;
p256dh: string;
auth: string;
}
>('/registerPushSubscription', async (req, res, next) => {
try {
const userPushSubRepository = getRepository(UserPushSubscription);
const existingSubs = await userPushSubRepository.find({
where: { auth: req.body.auth },
});
if (existingSubs.length > 0) {
logger.debug(
'User push subscription already exists. Skipping registration.',
{ label: 'API' }
);
return res.status(204).send();
}
const userPushSubscription = new UserPushSubscription({
auth: req.body.auth,
endpoint: req.body.endpoint,
p256dh: req.body.p256dh,
user: req.user,
});
userPushSubRepository.save(userPushSubscription);
return res.status(204).send();
} catch (e) {
logger.error('Failed to register user push subscription', {
label: 'API',
});
next({ status: 500, message: 'Failed to register subscription.' });
}
});
router.get<{ id: string }>('/:id', async (req, res, next) => {
try {
const userRepository = getRepository(User);

View File

@@ -7,7 +7,6 @@ import {
UserSettingsGeneralResponse,
UserSettingsNotificationsResponse,
} from '../../interfaces/api/userSettingsInterfaces';
import { NotificationAgentType } from '../../lib/notifications/agenttypes';
import { Permission } from '../../lib/permissions';
import { getSettings } from '../../lib/settings';
import logger from '../../logger';
@@ -52,6 +51,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
return res.status(200).json({
username: user.username,
locale: user.settings?.locale,
region: user.settings?.region,
originalLanguage: user.settings?.originalLanguage,
movieQuotaLimit: user.movieQuotaLimit,
@@ -109,17 +109,24 @@ userSettingsRoutes.post<
if (!user.settings) {
user.settings = new UserSettings({
user: req.user,
locale: req.body.locale,
region: req.body.region,
originalLanguage: req.body.originalLanguage,
});
} else {
user.settings.region = req.body.region;
(user.settings.locale = req.body.locale),
(user.settings.region = req.body.region);
user.settings.originalLanguage = req.body.originalLanguage;
}
await userRepository.save(user);
return res.status(200).json({ username: user.username });
return res.status(200).json({
username: user.username,
region: user.settings.region,
locale: user.settings.locale,
originalLanguage: user.settings.originalLanguage,
});
} catch (e) {
next({ status: 500, message: e.message });
}
@@ -243,8 +250,6 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
}
return res.status(200).json({
notificationAgents:
user.settings?.notificationAgents ?? NotificationAgentType.EMAIL,
emailEnabled: settings?.notifications.agents.email.enabled,
pgpKey: user.settings?.pgpKey,
discordEnabled: settings?.notifications.agents.discord.enabled,
@@ -254,6 +259,8 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
settings?.notifications.agents.telegram.options.botUsername,
telegramChatId: user.settings?.telegramChatId,
telegramSendSilently: user?.settings?.telegramSendSilently,
webPushEnabled: settings?.notifications.agents.webpush.enabled,
notificationTypes: user.settings?.notificationTypes ?? {},
});
} catch (e) {
next({ status: 500, message: e.message });
@@ -287,30 +294,32 @@ userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
if (!user.settings) {
user.settings = new UserSettings({
user: req.user,
notificationAgents:
req.body.notificationAgents ?? NotificationAgentType.EMAIL,
pgpKey: req.body.pgpKey,
discordId: req.body.discordId,
telegramChatId: req.body.telegramChatId,
telegramSendSilently: req.body.telegramSendSilently,
notificationTypes: req.body.notificationTypes,
});
} else {
user.settings.notificationAgents =
req.body.notificationAgents ?? NotificationAgentType.EMAIL;
user.settings.pgpKey = req.body.pgpKey;
user.settings.discordId = req.body.discordId;
user.settings.telegramChatId = req.body.telegramChatId;
user.settings.telegramSendSilently = req.body.telegramSendSilently;
user.settings.notificationTypes = Object.assign(
{},
user.settings.notificationTypes,
req.body.notificationTypes
);
}
userRepository.save(user);
return res.status(200).json({
notificationAgents: user.settings?.notificationAgents,
pgpKey: user.settings?.pgpKey,
discordId: user.settings?.discordId,
telegramChatId: user.settings?.telegramChatId,
telegramSendSilently: user?.settings?.telegramSendSilently,
notificationTypes: user.settings.notificationTypes,
});
} catch (e) {
next({ status: 500, message: e.message });

View File

@@ -6,6 +6,7 @@ declare global {
namespace Express {
export interface Request {
user?: User;
locale?: string;
}
}