From 2b27a715b07c27200ba1e5e9623629a34389276d Mon Sep 17 00:00:00 2001 From: Danshil Mungur Date: Sat, 26 Dec 2020 13:21:46 +0400 Subject: [PATCH 01/43] fix(docs): fix typo in build instructions (#503) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99ba8dd3d..0da054764 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ All help is welcome and greatly appreciated. If you would like to contribute to ``` yarn - yarn install + yarn dev ``` - Alternatively you can run using [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly. From 46af705014e6550171061526bc6ce077a378d48a Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 26 Dec 2020 18:22:28 +0900 Subject: [PATCH 02/43] docs: add danshilm as a contributor (#505) [skip ci] * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 50d69cf15..2f35147f3 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -196,7 +196,8 @@ "avatar_url": "https://avatars2.githubusercontent.com/u/20923978?v=4", "profile": "https://github.com/danshilm", "contributions": [ - "code" + "code", + "doc" ] }, { diff --git a/README.md b/README.md index 2707710ae..163422fcb 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Paul Hagedorn

🌍
Shagon94

🌍
sebstrgg

🌍 -
Danshil Mungur

💻 +
Danshil Mungur

💻 📖
doob187

🚇 From 7434a26f76b5e9f74918f3e1a34443d20ecfcbe4 Mon Sep 17 00:00:00 2001 From: Jakob Ankarhem Date: Sat, 26 Dec 2020 15:43:22 +0100 Subject: [PATCH 03/43] feat(frontend): add clear-field-icon to search field (#498) * feat(frontend): add button to clear search field Clear input field button was not visible on all devices, this replaces native ones with an svg * refactor(search): use tailwind css for button and change svg * refactor(search): larger click area on reset button Co-authored-by: Jakob Ankarhem --- src/assets/xcircle.svg | 1 + src/components/Layout/SearchInput/index.tsx | 14 ++++++++++++-- src/styles/globals.css | 4 ++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/assets/xcircle.svg diff --git a/src/assets/xcircle.svg b/src/assets/xcircle.svg new file mode 100644 index 000000000..6fee85051 --- /dev/null +++ b/src/assets/xcircle.svg @@ -0,0 +1 @@ + diff --git a/src/components/Layout/SearchInput/index.tsx b/src/components/Layout/SearchInput/index.tsx index 60e9f62af..643235ae8 100644 --- a/src/components/Layout/SearchInput/index.tsx +++ b/src/components/Layout/SearchInput/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import useSearchInput from '../../../hooks/useSearchInput'; import { defineMessages, useIntl } from 'react-intl'; +import ClearButton from '../../../assets/xcircle.svg'; const messages = defineMessages({ searchPlaceholder: 'Search Movies & TV', @@ -8,7 +9,7 @@ const messages = defineMessages({ const SearchInput: React.FC = () => { const intl = useIntl(); - const { searchValue, setSearchValue, setIsOpen } = useSearchInput(); + const { searchValue, setSearchValue, setIsOpen, clear } = useSearchInput(); return (
@@ -27,7 +28,8 @@ const SearchInput: React.FC = () => {
0 ? '1.75rem' : '' }} + className="block w-full h-full pl-8 py-2 rounded-md border-transparent focus:border-transparent bg-gray-600 text-white placeholder-gray-300 focus:outline-none focus:ring-0 focus:placeholder-gray-400 sm:text-base" placeholder={intl.formatMessage(messages.searchPlaceholder)} type="search" value={searchValue} @@ -35,6 +37,14 @@ const SearchInput: React.FC = () => { onFocus={() => setIsOpen(true)} onBlur={() => setIsOpen(false)} /> + {searchValue.length > 0 && ( + + )}
diff --git a/src/styles/globals.css b/src/styles/globals.css index 3517a6a62..bd248bf0c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -56,3 +56,7 @@ body { code { @apply px-2 py-1 bg-gray-800 rounded-md; } + +input[type='search']::-webkit-search-cancel-button { + -webkit-appearance: none; +} From c8d4d674f412082ad9e9da09abd79660365cf728 Mon Sep 17 00:00:00 2001 From: Jakob Ankarhem Date: Sat, 26 Dec 2020 16:02:00 +0100 Subject: [PATCH 04/43] feat(frontend): add telegram integration (#491) * feat(frontend): add telegram notification agent * feat(telegram): add i18n keys for telegram * style(telegram): change message formatting in notification * feat(telegram): add short tutorial for telegram setup * feat(telegram): add i18n keys for telegram tutorial * style(telegram): correct grammar in infobox Co-authored-by: sct * fix(telegram): redo i18n extraction Co-authored-by: Jakob Ankarhem Co-authored-by: sct --- overseerr-api.yml | 62 +++++ server/index.ts | 2 + server/lib/notifications/agents/telegram.ts | 128 ++++++++++ server/lib/settings.ts | 16 ++ server/routes/settings.ts | 35 +++ src/assets/extlogos/telegram.svg | 3 + .../Notifications/NotificationsDiscord.tsx | 2 +- .../Notifications/NotificationsEmail.tsx | 4 +- .../Notifications/NotificationsTelegram.tsx | 231 ++++++++++++++++++ .../Settings/SettingsNotifications.tsx | 12 + src/i18n/locale/en.json | 8 + src/pages/settings/notifications/telegram.tsx | 17 ++ 12 files changed, 517 insertions(+), 3 deletions(-) create mode 100644 server/lib/notifications/agents/telegram.ts create mode 100644 src/assets/extlogos/telegram.svg create mode 100644 src/components/Settings/Notifications/NotificationsTelegram.tsx create mode 100644 src/pages/settings/notifications/telegram.tsx diff --git a/overseerr-api.yml b/overseerr-api.yml index 6fe132187..627836adc 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -855,6 +855,22 @@ components: properties: webhookUrl: type: string + TelegramSettings: + type: object + properties: + enabled: + type: boolean + example: false + types: + type: number + example: 2 + options: + type: object + properties: + botAPI: + type: string + chatId: + type: string NotificationEmailSettings: type: object properties: @@ -1635,6 +1651,52 @@ paths: responses: '204': description: Test notification attempted + /settings/notifications/telegram: + get: + summary: Return current telegram notification settings + description: Returns current telegram notification settings in JSON format + tags: + - settings + responses: + '200': + description: Returned telegram settings + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + post: + summary: Update telegram notification settings + description: Update current telegram notification settings with provided values + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + responses: + '200': + description: 'Values were sucessfully updated' + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + /settings/notifications/telegram/test: + post: + summary: Test the provided telegram settings + description: Sends a test notification to the telegram agent + tags: + - settings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TelegramSettings' + responses: + '204': + description: Test notification attempted /settings/notifications/slack: get: summary: Return current slack notification settings diff --git a/server/index.ts b/server/index.ts index 76371a94c..eaf36833e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -17,6 +17,7 @@ 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'; @@ -47,6 +48,7 @@ app new DiscordAgent(), new EmailAgent(), new SlackAgent(), + new TelegramAgent(), ]); // Start Jobs diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts new file mode 100644 index 000000000..9d9e12705 --- /dev/null +++ b/server/lib/notifications/agents/telegram.ts @@ -0,0 +1,128 @@ +import axios from 'axios'; +import { Notification } from '..'; +import logger from '../../../logger'; +import { getSettings, NotificationAgentTelegram } from '../../settings'; +import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; + +interface TelegramPayload { + text: string; + parse_mode: string; + chat_id: string; +} + +class TelegramAgent + extends BaseAgent + implements NotificationAgent { + private baseUrl = 'https://api.telegram.org/'; + + protected getSettings(): NotificationAgentTelegram { + if (this.settings) { + return this.settings; + } + + const settings = getSettings(); + + return settings.notifications.agents.telegram; + } + + // TODO: Add checking for type here once we add notification type filters for agents + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public shouldSend(_type: Notification): boolean { + if ( + this.getSettings().enabled && + this.getSettings().options.botAPI && + this.getSettings().options.chatId + ) { + return true; + } + + return false; + } + + private escapeText(text: string | undefined): string { + return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : ''; + } + + private buildMessage( + type: Notification, + payload: NotificationPayload + ): string { + const settings = getSettings(); + let message = ''; + + const title = this.escapeText(payload.subject); + const plot = this.escapeText(payload.message); + const user = this.escapeText(payload.notifyUser.username); + + /* eslint-disable no-useless-escape */ + switch (type) { + case Notification.MEDIA_PENDING: + message += `\*New Request\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n\n`; + message += `\*Status\*\nPending Approval\n`; + + break; + case Notification.MEDIA_APPROVED: + message += `\*Request Approved\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n\n`; + message += `\*Status\*\nProcessing Request\n`; + + break; + case Notification.MEDIA_AVAILABLE: + message += `\*Now available\\!\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n\n`; + message += `\*Status\*\nAvailable\n`; + + break; + case Notification.TEST_NOTIFICATION: + message += `\*Test Notification\*\n`; + message += `${title}\n\n`; + message += `${plot}\n\n`; + message += `\*Requested By\*\n${user}\n`; + + break; + } + + if (settings.main.applicationUrl && payload.media) { + const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; + message += `\[Open in Overseerr\]\(${actionUrl}\)`; + } + /* eslint-enable */ + + return message; + } + + public async send( + type: Notification, + payload: NotificationPayload + ): Promise { + logger.debug('Sending telegram notification', { label: 'Notifications' }); + try { + const endpoint = `${this.baseUrl}bot${ + this.getSettings().options.botAPI + }/sendMessage`; + + await axios.post(endpoint, { + text: this.buildMessage(type, payload), + parse_mode: 'MarkdownV2', + chat_id: `${this.getSettings().options.chatId}`, + } as TelegramPayload); + + return true; + } catch (e) { + logger.error('Error sending Telegram notification', { + label: 'Notifications', + message: e.message, + }); + return false; + } + } +} + +export default TelegramAgent; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index c3cdfee66..1d25be5d2 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -84,10 +84,18 @@ export interface NotificationAgentEmail extends NotificationAgentConfig { }; } +export interface NotificationAgentTelegram extends NotificationAgentConfig { + options: { + botAPI: string; + chatId: string; + }; +} + interface NotificationAgents { email: NotificationAgentEmail; discord: NotificationAgentDiscord; slack: NotificationAgentSlack; + telegram: NotificationAgentTelegram; } interface NotificationSettings { @@ -156,6 +164,14 @@ class Settings { webhookUrl: '', }, }, + telegram: { + enabled: false, + types: 0, + options: { + botAPI: '', + chatId: '', + }, + }, }, }, }; diff --git a/server/routes/settings.ts b/server/routes/settings.ts index 4f22fe01f..ba9b91bc1 100644 --- a/server/routes/settings.ts +++ b/server/routes/settings.ts @@ -25,6 +25,7 @@ import { Notification } from '../lib/notifications'; import DiscordAgent from '../lib/notifications/agents/discord'; import EmailAgent from '../lib/notifications/agents/email'; import SlackAgent from '../lib/notifications/agents/slack'; +import TelegramAgent from '../lib/notifications/agents/telegram'; const settingsRoutes = Router(); @@ -503,6 +504,40 @@ settingsRoutes.post('/notifications/slack/test', (req, res, next) => { return res.status(204).send(); }); +settingsRoutes.get('/notifications/telegram', (_req, res) => { + const settings = getSettings(); + + res.status(200).json(settings.notifications.agents.telegram); +}); + +settingsRoutes.post('/notifications/telegram', (req, res) => { + const settings = getSettings(); + + settings.notifications.agents.telegram = req.body; + settings.save(); + + res.status(200).json(settings.notifications.agents.telegram); +}); + +settingsRoutes.post('/notifications/telegram/test', (req, res, next) => { + if (!req.user) { + return next({ + status: 500, + message: 'User information missing from request', + }); + } + + const telegramAgent = new TelegramAgent(req.body); + telegramAgent.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(); +}); + settingsRoutes.get('/notifications/email', (_req, res) => { const settings = getSettings(); diff --git a/src/assets/extlogos/telegram.svg b/src/assets/extlogos/telegram.svg new file mode 100644 index 000000000..d10e5c88b --- /dev/null +++ b/src/assets/extlogos/telegram.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Settings/Notifications/NotificationsDiscord.tsx b/src/components/Settings/Notifications/NotificationsDiscord.tsx index 6bf001e7b..2c7b23f49 100644 --- a/src/components/Settings/Notifications/NotificationsDiscord.tsx +++ b/src/components/Settings/Notifications/NotificationsDiscord.tsx @@ -89,7 +89,7 @@ const NotificationsDiscord: React.FC = () => {
+
+ +
+
+ +
+
+