feat(telegram): add support for individual chat notifications (#1027)

pull/1093/head
Jakob Ankarhem 4 years ago committed by GitHub
parent 6072e8aa9a
commit f6d00d8d15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -35,6 +35,7 @@ These variables are usually the target user of the notification.
- `{{notifyuser_email}}` Target user's email. - `{{notifyuser_email}}` Target user's email.
- `{{notifyuser_avatar}}` Target user's avatar. - `{{notifyuser_avatar}}` Target user's avatar.
- `{{notifyuser_settings_discordId}}` Target user's discord ID (if one is set). - `{{notifyuser_settings_discordId}}` Target user's discord ID (if one is set).
- `{{notifyuser_settings_telegramChatId}}` Target user's telegram Chat ID (if one is set).
### Media ### Media

@ -97,6 +97,10 @@ components:
default: true default: true
discordId: discordId:
type: string type: string
telegramChatId:
type: string
telegramSendSilently:
type: boolean
required: required:
- enableNotifications - enableNotifications
MainSettings: MainSettings:
@ -1545,6 +1549,12 @@ components:
discordId: discordId:
type: string type: string
nullable: true nullable: true
telegramChatId:
type: string
nullable: true
telegramSendSilently:
type: boolean
nullable: true
required: required:
- enableNotifications - enableNotifications
securitySchemes: securitySchemes:

@ -26,6 +26,12 @@ export class UserSettings {
@Column({ nullable: true }) @Column({ nullable: true })
public discordId?: string; public discordId?: string;
@Column({ nullable: true })
public telegramChatId?: string;
@Column({ nullable: true })
public telegramSendSilently?: boolean;
@Column({ nullable: true }) @Column({ nullable: true })
public region?: string; public region?: string;

@ -6,5 +6,8 @@ export interface UserSettingsGeneralResponse {
export interface UserSettingsNotificationsResponse { export interface UserSettingsNotificationsResponse {
enableNotifications: boolean; enableNotifications: boolean;
telegramBotUsername?: string;
discordId?: string; discordId?: string;
telegramChatId?: string;
telegramSendSilently?: boolean;
} }

@ -134,6 +134,21 @@ class TelegramAgent
disable_notification: this.getSettings().options.sendSilently, disable_notification: this.getSettings().options.sendSilently,
} as TelegramPayload); } as TelegramPayload);
if (
payload.notifyUser.settings?.enableNotifications &&
payload.notifyUser.settings?.telegramChatId &&
payload.notifyUser.settings?.telegramChatId !==
this.getSettings().options.chatId
) {
await axios.post(endpoint, {
text: this.buildMessage(type, payload),
parse_mode: 'MarkdownV2',
chat_id: `${payload.notifyUser.settings.telegramChatId}`,
disable_notification:
payload.notifyUser.settings.telegramSendSilently,
} as TelegramPayload);
}
return true; return true;
} catch (e) { } catch (e) {
logger.error('Error sending Telegram notification', { logger.error('Error sending Telegram notification', {

@ -20,6 +20,7 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
notifyuser_email: 'notifyUser.email', notifyuser_email: 'notifyUser.email',
notifyuser_avatar: 'notifyUser.avatar', notifyuser_avatar: 'notifyUser.avatar',
notifyuser_settings_discordId: 'notifyUser.settings.discordId', notifyuser_settings_discordId: 'notifyUser.settings.discordId',
notifyuser_settings_telegramChatId: 'notifyUser.settings.telegramChatId',
media_tmdbid: 'media.tmdbId', media_tmdbid: 'media.tmdbId',
media_imdbid: 'media.imdbId', media_imdbid: 'media.imdbId',
media_tvdbid: 'media.tvdbId', media_tvdbid: 'media.tvdbId',

@ -120,6 +120,7 @@ export interface NotificationAgentEmail extends NotificationAgentConfig {
export interface NotificationAgentTelegram extends NotificationAgentConfig { export interface NotificationAgentTelegram extends NotificationAgentConfig {
options: { options: {
botUsername: string;
botAPI: string; botAPI: string;
chatId: string; chatId: string;
sendSilently: boolean; sendSilently: boolean;
@ -242,6 +243,7 @@ class Settings {
enabled: false, enabled: false,
types: 0, types: 0,
options: { options: {
botUsername: '',
botAPI: '', botAPI: '',
chatId: '', chatId: '',
sendSilently: false, sendSilently: false,

@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddTelegramSettingsToUserSettings1614334195680
implements MigrationInterface {
name = 'AddTelegramSettingsToUserSettings1614334195680';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, 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", "enableNotifications", "discordId", "userId", "region", "originalLanguage") SELECT "id", "enableNotifications", "discordId", "userId", "region", "originalLanguage" 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, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" 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", "enableNotifications", "discordId", "userId", "region", "originalLanguage") SELECT "id", "enableNotifications", "discordId", "userId", "region", "originalLanguage" FROM "temporary_user_settings"`
);
await queryRunner.query(`DROP TABLE "temporary_user_settings"`);
}
}

@ -2,6 +2,7 @@ import { Router } from 'express';
import { getRepository } from 'typeorm'; import { getRepository } from 'typeorm';
import { canMakePermissionsChange } from '.'; import { canMakePermissionsChange } from '.';
import { User } from '../../entity/User'; import { User } from '../../entity/User';
import { getSettings } from '../../lib/settings';
import { UserSettings } from '../../entity/UserSettings'; import { UserSettings } from '../../entity/UserSettings';
import { import {
UserSettingsGeneralResponse, UserSettingsGeneralResponse,
@ -198,6 +199,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
isOwnProfileOrAdmin(), isOwnProfileOrAdmin(),
async (req, res, next) => { async (req, res, next) => {
const userRepository = getRepository(User); const userRepository = getRepository(User);
const settings = getSettings();
try { try {
const user = await userRepository.findOne({ const user = await userRepository.findOne({
@ -210,7 +212,11 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
return res.status(200).json({ return res.status(200).json({
enableNotifications: user.settings?.enableNotifications ?? true, enableNotifications: user.settings?.enableNotifications ?? true,
telegramBotUsername:
settings?.notifications.agents.telegram.options.botUsername,
discordId: user.settings?.discordId, discordId: user.settings?.discordId,
telegramChatId: user.settings?.telegramChatId,
telegramSendSilently: user?.settings?.telegramSendSilently,
}); });
} catch (e) { } catch (e) {
next({ status: 500, message: e.message }); next({ status: 500, message: e.message });
@ -239,10 +245,14 @@ userSettingsRoutes.post<
user: req.user, user: req.user,
enableNotifications: req.body.enableNotifications, enableNotifications: req.body.enableNotifications,
discordId: req.body.discordId, discordId: req.body.discordId,
telegramChatId: req.body.telegramChatId,
telegramSendSilently: req.body.telegramSendSilently,
}); });
} else { } else {
user.settings.enableNotifications = req.body.enableNotifications; user.settings.enableNotifications = req.body.enableNotifications;
user.settings.discordId = req.body.discordId; user.settings.discordId = req.body.discordId;
user.settings.telegramChatId = req.body.telegramChatId;
user.settings.telegramSendSilently = req.body.telegramSendSilently;
} }
userRepository.save(user); userRepository.save(user);
@ -250,6 +260,8 @@ userSettingsRoutes.post<
return res.status(200).json({ return res.status(200).json({
enableNotifications: user.settings.enableNotifications, enableNotifications: user.settings.enableNotifications,
discordId: user.settings.discordId, discordId: user.settings.discordId,
telegramChatId: user.settings.telegramChatId,
telegramSendSilently: user.settings.telegramSendSilently,
}); });
} catch (e) { } catch (e) {
next({ status: 500, message: e.message }); next({ status: 500, message: e.message });

@ -14,6 +14,7 @@ const messages = defineMessages({
save: 'Save Changes', save: 'Save Changes',
saving: 'Saving…', saving: 'Saving…',
agentenabled: 'Enable Agent', agentenabled: 'Enable Agent',
botUsername: 'Bot Username',
botAPI: 'Bot Authentication Token', botAPI: 'Bot Authentication Token',
chatId: 'Chat ID', chatId: 'Chat ID',
validationBotAPIRequired: 'You must provide a bot authentication token', validationBotAPIRequired: 'You must provide a bot authentication token',
@ -43,7 +44,10 @@ const NotificationsTelegram: React.FC = () => {
botAPI: Yup.string().required( botAPI: Yup.string().required(
intl.formatMessage(messages.validationBotAPIRequired) intl.formatMessage(messages.validationBotAPIRequired)
), ),
chatId: Yup.string().required( chatId: Yup.string()
.required(intl.formatMessage(messages.validationChatIdRequired))
.matches(
/^[-]?\d+$/,
intl.formatMessage(messages.validationChatIdRequired) intl.formatMessage(messages.validationChatIdRequired)
), ),
}); });
@ -57,6 +61,7 @@ const NotificationsTelegram: React.FC = () => {
initialValues={{ initialValues={{
enabled: data?.enabled, enabled: data?.enabled,
types: data?.types, types: data?.types,
botUsername: data?.options.botUsername,
botAPI: data?.options.botAPI, botAPI: data?.options.botAPI,
chatId: data?.options.chatId, chatId: data?.options.chatId,
sendSilently: data?.options.sendSilently, sendSilently: data?.options.sendSilently,
@ -71,6 +76,7 @@ const NotificationsTelegram: React.FC = () => {
botAPI: values.botAPI, botAPI: values.botAPI,
chatId: values.chatId, chatId: values.chatId,
sendSilently: values.sendSilently, sendSilently: values.sendSilently,
botUsername: values.botUsername,
}, },
}); });
addToast(intl.formatMessage(messages.telegramsettingssaved), { addToast(intl.formatMessage(messages.telegramsettingssaved), {
@ -96,6 +102,7 @@ const NotificationsTelegram: React.FC = () => {
botAPI: values.botAPI, botAPI: values.botAPI,
chatId: values.chatId, chatId: values.chatId,
sendSilently: values.sendSilently, sendSilently: values.sendSilently,
botUsername: values.botUsername,
}, },
}); });
@ -147,6 +154,24 @@ const NotificationsTelegram: React.FC = () => {
<Field type="checkbox" id="enabled" name="enabled" /> <Field type="checkbox" id="enabled" name="enabled" />
</div> </div>
</div> </div>
<div className="form-row">
<label htmlFor="botUsername" className="text-label">
{intl.formatMessage(messages.botUsername)}
</label>
<div className="form-input">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="botUsername"
name="botUsername"
type="text"
placeholder={intl.formatMessage(messages.botUsername)}
/>
</div>
{errors.botUsername && touched.botUsername && (
<div className="error">{errors.botUsername}</div>
)}
</div>
</div>
<div className="form-row"> <div className="form-row">
<label htmlFor="botAPI" className="text-label"> <label htmlFor="botAPI" className="text-label">
{intl.formatMessage(messages.botAPI)} {intl.formatMessage(messages.botAPI)}

@ -19,6 +19,15 @@ const messages = defineMessages({
discordIdTip: discordIdTip:
'The <FindDiscordIdLink>ID number</FindDiscordIdLink> for your Discord user account', 'The <FindDiscordIdLink>ID number</FindDiscordIdLink> for your Discord user account',
validationDiscordId: 'You must provide a valid Discord user ID', validationDiscordId: 'You must provide a valid Discord user ID',
telegramChatId: 'Telegram Chat ID',
telegramChatIdTip:
'The Chat ID can be aquired by adding <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat.',
telegramChatIdTipLong:
'Start a chat by clicking <TelegramBotLink>here</TelegramBotLink>.\
Then get the group Chat ID by adding <GetIdBotLink>@get_id_bot</GetIdBotLink> to that chat and send /my_id to the chat',
sendSilently: 'Send Silently',
sendSilentlyDescription: 'Send telegram notifications silently',
validationTelegramChatId: 'You must provide a valid Telegram Chat ID',
save: 'Save Changes', save: 'Save Changes',
saving: 'Saving…', saving: 'Saving…',
plexuser: 'Plex User', plexuser: 'Plex User',
@ -40,6 +49,12 @@ const UserNotificationSettings: React.FC = () => {
discordId: Yup.string() discordId: Yup.string()
.optional() .optional()
.matches(/^\d{17,18}$/, intl.formatMessage(messages.validationDiscordId)), .matches(/^\d{17,18}$/, intl.formatMessage(messages.validationDiscordId)),
telegramChatId: Yup.string()
.optional()
.matches(
/^[-]?\d+$/,
intl.formatMessage(messages.validationTelegramChatId)
),
}); });
if (!data && !error) { if (!data && !error) {
@ -61,6 +76,8 @@ const UserNotificationSettings: React.FC = () => {
initialValues={{ initialValues={{
enableNotifications: data?.enableNotifications, enableNotifications: data?.enableNotifications,
discordId: data?.discordId, discordId: data?.discordId,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
}} }}
validationSchema={UserNotificationSettingsSchema} validationSchema={UserNotificationSettingsSchema}
enableReinitialize enableReinitialize
@ -71,6 +88,8 @@ const UserNotificationSettings: React.FC = () => {
{ {
enableNotifications: values.enableNotifications, enableNotifications: values.enableNotifications,
discordId: values.discordId, discordId: values.discordId,
telegramChatId: values.telegramChatId,
telegramSendSilently: values.telegramSendSilently,
} }
); );
@ -135,6 +154,86 @@ const UserNotificationSettings: React.FC = () => {
)} )}
</div> </div>
</div> </div>
<div className="form-row">
<label htmlFor="telegramChatId" className="text-label">
<span>{intl.formatMessage(messages.telegramChatId)}</span>
<span className="label-tip">
{data?.telegramBotUsername
? intl.formatMessage(messages.telegramChatIdTipLong, {
TelegramBotLink: function TelegramBotLink(msg) {
return (
<a
href={`https://telegram.me/${data.telegramBotUsername}`}
target="_blank"
rel="noreferrer"
className="text-gray-100 underline transition duration-300 hover:text-white"
>
{msg}
</a>
);
},
GetIdBotLink: function GetIdBotLink(msg) {
return (
<a
href="https://telegram.me/get_id_bot"
className="text-gray-100 underline transition duration-300 hover:text-white"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})
: intl.formatMessage(messages.telegramChatIdTip, {
GetIdBotLink: function GetIdBotLink(msg) {
return (
<a
href="https://telegram.me/get_id_bot"
className="text-gray-100 underline transition duration-300 hover:text-white"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
})}
</span>
</label>
<div className="form-input">
<div className="flex max-w-lg rounded-md shadow-sm">
<Field
id="telegramChatId"
name="telegramChatId"
type="text"
/>
</div>
{errors.telegramChatId && touched.telegramChatId && (
<div className="error">{errors.telegramChatId}</div>
)}
</div>
</div>
<div className="form-row">
<label
htmlFor="telegramSendSilently"
className="checkbox-label"
>
<span className="mr-2">
{intl.formatMessage(messages.sendSilently)}
</span>
<span className="label-tip">
{intl.formatMessage(messages.sendSilentlyDescription)}
</span>
</label>
<div className="form-input">
<Field
type="checkbox"
id="telegramSendSilently"
name="telegramSendSilently"
/>
</div>
</div>
<div className="actions"> <div className="actions">
<div className="flex justify-end"> <div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm"> <span className="inline-flex ml-3 rounded-md shadow-sm">

@ -306,6 +306,7 @@
"components.Settings.Notifications.authPass": "SMTP Password", "components.Settings.Notifications.authPass": "SMTP Password",
"components.Settings.Notifications.authUser": "SMTP Username", "components.Settings.Notifications.authUser": "SMTP Username",
"components.Settings.Notifications.botAPI": "Bot Authentication Token", "components.Settings.Notifications.botAPI": "Bot Authentication Token",
"components.Settings.Notifications.botUsername": "Bot Username",
"components.Settings.Notifications.chatId": "Chat ID", "components.Settings.Notifications.chatId": "Chat ID",
"components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.", "components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.",
"components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!", "components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!",
@ -718,9 +719,15 @@
"components.UserProfile.UserSettings.UserNotificationSettings.plexuser": "Plex User", "components.UserProfile.UserSettings.UserNotificationSettings.plexuser": "Plex User",
"components.UserProfile.UserSettings.UserNotificationSettings.save": "Save Changes", "components.UserProfile.UserSettings.UserNotificationSettings.save": "Save Changes",
"components.UserProfile.UserSettings.UserNotificationSettings.saving": "Saving…", "components.UserProfile.UserSettings.UserNotificationSettings.saving": "Saving…",
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Send Silently",
"components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Send telegram notifications silently",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Telegram Chat ID",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTip": "The Chat ID can be aquired by adding <GetIdBotLink>@get_id_bot</GetIdBotLink> to the chat.",
"components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "Start a chat by clicking <TelegramBotLink>here</TelegramBotLink>. Then get the group Chat ID by adding <GetIdBotLink>@get_id_bot</GetIdBotLink> to that chat and send /my_id to the chat",
"components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsFailure": "Something went wrong while saving settings.", "components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsFailure": "Something went wrong while saving settings.",
"components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsSuccess": "Settings successfully saved!", "components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsSuccess": "Settings successfully saved!",
"components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "You must provide a valid Discord user ID", "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "You must provide a valid Discord user ID",
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid Telegram Chat ID",
"components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirm Password", "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirm Password",
"components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Current Password", "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Current Password",
"components.UserProfile.UserSettings.UserPasswordChange.newpassword": "New Password", "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "New Password",

Loading…
Cancel
Save