feat(notif): add Pushbullet and Pushover agents to user notification settings (#1740)
* feat(notif): add Pushbullet and Pushover agents to user notification settings * docs(notif): add "hint" about user notifications to Pushbullet and Pushover pages * fix: regenerate DB migrationpull/2233/head
parent
ab20c21184
commit
aeb7a48d72
@ -0,0 +1,33 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddPushbulletPushoverUserSettings1635079863457
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddPushbulletPushoverUserSettings1635079863457';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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 (''), "pushbulletAccessToken" varchar, "pushoverApplicationToken" varchar, "pushoverUserKey" 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 "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" text, "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, "locale" varchar NOT NULL DEFAULT (''), 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"`);
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
import axios from 'axios';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import SensitiveInput from '../../../Common/SensitiveInput';
|
||||
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
pushbulletsettingssaved:
|
||||
'Pushbullet notification settings saved successfully!',
|
||||
pushbulletsettingsfailed: 'Pushbullet notification settings failed to save.',
|
||||
pushbulletAccessToken: 'Access Token',
|
||||
pushbulletAccessTokenTip:
|
||||
'Create a token from your <PushbulletSettingsLink>Account Settings</PushbulletSettingsLink>',
|
||||
validationPushbulletAccessToken: 'You must provide an access token',
|
||||
});
|
||||
|
||||
const UserPushbulletSettings: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const router = useRouter();
|
||||
const { user } = useUser({ id: Number(router.query.userId) });
|
||||
const { data, error, revalidate } = useSWR<UserSettingsNotificationsResponse>(
|
||||
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
|
||||
);
|
||||
|
||||
const UserNotificationsPushbulletSchema = Yup.object().shape({
|
||||
pushbulletAccessToken: Yup.string().when('types', {
|
||||
is: (types: number) => !!types,
|
||||
then: Yup.string()
|
||||
.nullable()
|
||||
.required(intl.formatMessage(messages.validationPushbulletAccessToken)),
|
||||
otherwise: Yup.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
pushbulletAccessToken: data?.pushbulletAccessToken,
|
||||
types: data?.notificationTypes.pushbullet ?? 0,
|
||||
}}
|
||||
validationSchema={UserNotificationsPushbulletSchema}
|
||||
enableReinitialize
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
|
||||
pgpKey: data?.pgpKey,
|
||||
discordId: data?.discordId,
|
||||
pushbulletAccessToken: values.pushbulletAccessToken,
|
||||
pushoverApplicationToken: data?.pushoverApplicationToken,
|
||||
pushoverUserKey: data?.pushoverUserKey,
|
||||
telegramChatId: data?.telegramChatId,
|
||||
telegramSendSilently: data?.telegramSendSilently,
|
||||
notificationTypes: {
|
||||
pushbullet: values.types,
|
||||
},
|
||||
});
|
||||
addToast(intl.formatMessage(messages.pushbulletsettingssaved), {
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.pushbulletsettingsfailed), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} finally {
|
||||
revalidate();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
values,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
return (
|
||||
<Form className="section">
|
||||
<div className="form-row">
|
||||
<label htmlFor="pushbulletAccessToken" className="text-label">
|
||||
{intl.formatMessage(messages.pushbulletAccessToken)}
|
||||
<span className="label-required">*</span>
|
||||
{data?.pushbulletAccessToken && (
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.pushbulletAccessTokenTip, {
|
||||
PushbulletSettingsLink: function PushbulletSettingsLink(
|
||||
msg
|
||||
) {
|
||||
return (
|
||||
<a
|
||||
href="https://www.pushbullet.com/#settings/account"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
<SensitiveInput
|
||||
as="field"
|
||||
id="pushbulletAccessToken"
|
||||
name="pushbulletAccessToken"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.pushbulletAccessToken &&
|
||||
touched.pushbulletAccessToken && (
|
||||
<div className="error">{errors.pushbulletAccessToken}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
user={user}
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
disabled={isSubmitting || !isValid}
|
||||
>
|
||||
{isSubmitting
|
||||
? intl.formatMessage(globalMessages.saving)
|
||||
: intl.formatMessage(globalMessages.save)}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPushbulletSettings;
|
@ -0,0 +1,228 @@
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces';
|
||||
import { useUser } from '../../../../hooks/useUser';
|
||||
import globalMessages from '../../../../i18n/globalMessages';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import NotificationTypeSelector from '../../../NotificationTypeSelector';
|
||||
|
||||
const messages = defineMessages({
|
||||
pushoversettingssaved: 'Pushover notification settings saved successfully!',
|
||||
pushoversettingsfailed: 'Pushover notification settings failed to save.',
|
||||
pushoverApplicationToken: 'Application API Token',
|
||||
pushoverApplicationTokenTip:
|
||||
'<ApplicationRegistrationLink>Register an application</ApplicationRegistrationLink> for use with Overseerr',
|
||||
pushoverUserKey: 'User or Group Key',
|
||||
pushoverUserKeyTip:
|
||||
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
|
||||
validationPushoverApplicationToken:
|
||||
'You must provide a valid application token',
|
||||
validationPushoverUserKey: 'You must provide a valid user or group key',
|
||||
});
|
||||
|
||||
const UserPushoverSettings: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
const router = useRouter();
|
||||
const { user } = useUser({ id: Number(router.query.userId) });
|
||||
const { data, error, revalidate } = useSWR<UserSettingsNotificationsResponse>(
|
||||
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
|
||||
);
|
||||
|
||||
const UserNotificationsPushoverSchema = Yup.object().shape({
|
||||
pushoverApplicationToken: Yup.string()
|
||||
.when('types', {
|
||||
is: (types: number) => !!types,
|
||||
then: Yup.string()
|
||||
.nullable()
|
||||
.required(
|
||||
intl.formatMessage(messages.validationPushoverApplicationToken)
|
||||
),
|
||||
otherwise: Yup.string().nullable(),
|
||||
})
|
||||
.matches(
|
||||
/^[a-z\d]{30}$/i,
|
||||
intl.formatMessage(messages.validationPushoverApplicationToken)
|
||||
),
|
||||
pushoverUserKey: Yup.string()
|
||||
.when('types', {
|
||||
is: (types: number) => !!types,
|
||||
then: Yup.string()
|
||||
.nullable()
|
||||
.required(intl.formatMessage(messages.validationPushoverUserKey)),
|
||||
otherwise: Yup.string().nullable(),
|
||||
})
|
||||
.matches(
|
||||
/^[a-z\d]{30}$/i,
|
||||
intl.formatMessage(messages.validationPushoverUserKey)
|
||||
),
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
pushoverApplicationToken: data?.pushoverApplicationToken,
|
||||
pushoverUserKey: data?.pushoverUserKey,
|
||||
types: data?.notificationTypes.pushover ?? 0,
|
||||
}}
|
||||
validationSchema={UserNotificationsPushoverSchema}
|
||||
enableReinitialize
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
|
||||
pgpKey: data?.pgpKey,
|
||||
discordId: data?.discordId,
|
||||
pushbulletAccessToken: data?.pushbulletAccessToken,
|
||||
pushoverApplicationToken: values.pushoverApplicationToken,
|
||||
pushoverUserKey: values.pushoverUserKey,
|
||||
telegramChatId: data?.telegramChatId,
|
||||
telegramSendSilently: data?.telegramSendSilently,
|
||||
notificationTypes: {
|
||||
pushover: values.types,
|
||||
},
|
||||
});
|
||||
addToast(intl.formatMessage(messages.pushoversettingssaved), {
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.pushoversettingsfailed), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} finally {
|
||||
revalidate();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
values,
|
||||
setFieldValue,
|
||||
setFieldTouched,
|
||||
}) => {
|
||||
return (
|
||||
<Form className="section">
|
||||
<div className="form-row">
|
||||
<label htmlFor="pushoverApplicationToken" className="text-label">
|
||||
{intl.formatMessage(messages.pushoverApplicationToken)}
|
||||
<span className="label-required">*</span>
|
||||
{data?.pushoverApplicationToken && (
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.pushoverApplicationTokenTip, {
|
||||
ApplicationRegistrationLink:
|
||||
function ApplicationRegistrationLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://pushover.net/api#registration"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="pushoverApplicationToken"
|
||||
name="pushoverApplicationToken"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.pushoverApplicationToken &&
|
||||
touched.pushoverApplicationToken && (
|
||||
<div className="error">
|
||||
{errors.pushoverApplicationToken}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="pushoverUserKey" className="checkbox-label">
|
||||
{intl.formatMessage(messages.pushoverUserKey)}
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.pushoverUserKeyTip, {
|
||||
UsersGroupsLink: function UsersGroupsLink(msg) {
|
||||
return (
|
||||
<a
|
||||
href="https://pushover.net/api#identifiers"
|
||||
className="text-white transition duration-300 hover:underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{msg}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="pushoverUserKey"
|
||||
name="pushoverUserKey"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.pushoverUserKey && touched.pushoverUserKey && (
|
||||
<div className="error">{errors.pushoverUserKey}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<NotificationTypeSelector
|
||||
user={user}
|
||||
currentTypes={values.types}
|
||||
onUpdate={(newTypes) => {
|
||||
setFieldValue('types', newTypes);
|
||||
setFieldTouched('types');
|
||||
}}
|
||||
error={
|
||||
errors.types && touched.types
|
||||
? (errors.types as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="inline-flex ml-3 rounded-md shadow-sm">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
disabled={isSubmitting || !isValid}
|
||||
>
|
||||
{isSubmitting
|
||||
? intl.formatMessage(globalMessages.saving)
|
||||
: intl.formatMessage(globalMessages.save)}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPushoverSettings;
|
@ -0,0 +1,17 @@
|
||||
import { NextPage } from 'next';
|
||||
import React from 'react';
|
||||
import UserSettings from '../../../../components/UserProfile/UserSettings';
|
||||
import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
||||
import UserNotificationsPushbullet from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushbullet';
|
||||
|
||||
const NotificationsPage: NextPage = () => {
|
||||
return (
|
||||
<UserSettings>
|
||||
<UserNotificationSettings>
|
||||
<UserNotificationsPushbullet />
|
||||
</UserNotificationSettings>
|
||||
</UserSettings>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsPage;
|
@ -0,0 +1,17 @@
|
||||
import { NextPage } from 'next';
|
||||
import React from 'react';
|
||||
import UserSettings from '../../../../components/UserProfile/UserSettings';
|
||||
import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
||||
import UserNotificationsPushover from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover';
|
||||
|
||||
const NotificationsPage: NextPage = () => {
|
||||
return (
|
||||
<UserSettings>
|
||||
<UserNotificationSettings>
|
||||
<UserNotificationsPushover />
|
||||
</UserNotificationSettings>
|
||||
</UserSettings>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsPage;
|
@ -0,0 +1,20 @@
|
||||
import { NextPage } from 'next';
|
||||
import React from 'react';
|
||||
import UserSettings from '../../../../../components/UserProfile/UserSettings';
|
||||
import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
||||
import UserNotificationsPushbullet from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushbullet';
|
||||
import useRouteGuard from '../../../../../hooks/useRouteGuard';
|
||||
import { Permission } from '../../../../../hooks/useUser';
|
||||
|
||||
const NotificationsPage: NextPage = () => {
|
||||
useRouteGuard(Permission.MANAGE_USERS);
|
||||
return (
|
||||
<UserSettings>
|
||||
<UserNotificationSettings>
|
||||
<UserNotificationsPushbullet />
|
||||
</UserNotificationSettings>
|
||||
</UserSettings>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsPage;
|
@ -0,0 +1,20 @@
|
||||
import { NextPage } from 'next';
|
||||
import React from 'react';
|
||||
import UserSettings from '../../../../../components/UserProfile/UserSettings';
|
||||
import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings';
|
||||
import UserNotificationsPushover from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsPushover';
|
||||
import useRouteGuard from '../../../../../hooks/useRouteGuard';
|
||||
import { Permission } from '../../../../../hooks/useUser';
|
||||
|
||||
const NotificationsPage: NextPage = () => {
|
||||
useRouteGuard(Permission.MANAGE_USERS);
|
||||
return (
|
||||
<UserSettings>
|
||||
<UserNotificationSettings>
|
||||
<UserNotificationsPushover />
|
||||
</UserNotificationSettings>
|
||||
</UserSettings>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsPage;
|
Loading…
Reference in new issue