mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(notifications): add telegram support (#299)
* feat(notifications): add telegram support * feat(notifications): change list view * refactor(notifications): overall setup * feat(notifications): forms add telegram
This commit is contained in:
parent
2ab7133dd0
commit
38addb99e6
15 changed files with 630 additions and 457 deletions
|
@ -135,7 +135,8 @@ export const APIClient = {
|
|||
getAll: () => appClient.Get<Notification[]>("api/notification"),
|
||||
create: (notification: Notification) => appClient.Post("api/notification", notification),
|
||||
update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification),
|
||||
delete: (id: number) => appClient.Delete(`api/notification/${id}`)
|
||||
delete: (id: number) => appClient.Delete(`api/notification/${id}`),
|
||||
test: (n: Notification) => appClient.Post("api/notification/test", n)
|
||||
},
|
||||
release: {
|
||||
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
|
||||
|
|
|
@ -17,6 +17,7 @@ interface SlideOverProps<DataType> {
|
|||
children?: (values: DataType) => React.ReactNode;
|
||||
deleteAction?: () => void;
|
||||
type: "CREATE" | "UPDATE";
|
||||
testFn?: (data: unknown) => void;
|
||||
}
|
||||
|
||||
function SlideOver<DataType>({
|
||||
|
@ -28,11 +29,18 @@ function SlideOver<DataType>({
|
|||
isOpen,
|
||||
toggle,
|
||||
type,
|
||||
children
|
||||
children,
|
||||
testFn
|
||||
}: SlideOverProps<DataType>): React.ReactElement {
|
||||
const cancelModalButtonRef = useRef<HTMLInputElement | null>(null);
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
|
||||
const test = (values: unknown) => {
|
||||
if (testFn) {
|
||||
testFn(values);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
||||
|
@ -106,17 +114,26 @@ function SlideOver<DataType>({
|
|||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-white bg-red-100 dark:bg-red-700 hover:bg-red-200 dark:hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||
onClick={toggleDeleteModal}
|
||||
>
|
||||
Remove
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
<div>
|
||||
{testFn && (
|
||||
<button
|
||||
type="button"
|
||||
className="mr-2 bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
onClick={() => test(values)}
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
onClick={toggle}
|
||||
>
|
||||
Cancel
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
|
|
|
@ -260,6 +260,10 @@ export const NotificationTypeOptions: OptionBasic[] = [
|
|||
{
|
||||
label: "Discord",
|
||||
value: "DISCORD"
|
||||
},
|
||||
{
|
||||
label: "Telegram",
|
||||
value: "TELEGRAM"
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { FieldProps } from "formik";
|
|||
import { XIcon } from "@heroicons/react/solid";
|
||||
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
|
||||
import {
|
||||
PasswordFieldWide,
|
||||
SwitchGroupWide,
|
||||
TextFieldWide
|
||||
} from "../../components/inputs";
|
||||
|
@ -59,18 +60,17 @@ const Option = (props: OptionProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
function FormFieldsDiscord() {
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||
{/*<div className="px-6 space-y-1">*/}
|
||||
{/* <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Credentials</Dialog.Title>*/}
|
||||
{/* <p className="text-sm text-gray-500 dark:text-gray-400">*/}
|
||||
{/* Api keys etc*/}
|
||||
{/* </p>*/}
|
||||
{/*</div>*/}
|
||||
<div className="px-6 space-y-1">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Settings</Dialog.Title>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Create a <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" rel="noopener noreferrer" target="_blank" className="font-medium text-blue-500">webhook integration</a> in your server.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TextFieldWide
|
||||
<PasswordFieldWide
|
||||
name="webhook"
|
||||
label="Webhook URL"
|
||||
help="Discord channel webhook url"
|
||||
|
@ -80,8 +80,33 @@ function FormFieldsDiscord() {
|
|||
);
|
||||
}
|
||||
|
||||
function FormFieldsTelegram() {
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||
<div className="px-6 space-y-1">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Settings</Dialog.Title>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Read how to <a href="https://core.telegram.org/bots#3-how-do-i-create-a-bot" rel="noopener noreferrer" target="_blank" className="font-medium text-blue-500">create a bot</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PasswordFieldWide
|
||||
name="token"
|
||||
label="Bot token"
|
||||
help="Bot token"
|
||||
/>
|
||||
<PasswordFieldWide
|
||||
name="channel"
|
||||
label="Chat ID"
|
||||
help="Chat ID"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const componentMap: componentMapType = {
|
||||
DISCORD: <FormFieldsDiscord/>
|
||||
DISCORD: <FormFieldsDiscord/>,
|
||||
TELEGRAM: <FormFieldsTelegram/>
|
||||
};
|
||||
|
||||
interface NotificationAddFormValues {
|
||||
|
@ -113,6 +138,19 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) {
|
|||
mutation.mutate(formData as Notification);
|
||||
};
|
||||
|
||||
const testMutation = useMutation(
|
||||
(n: Notification) => APIClient.notifications.test(n),
|
||||
{
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const testNotification = (data: unknown) => {
|
||||
testMutation.mutate(data as Notification);
|
||||
};
|
||||
|
||||
const validate = (values: NotificationAddFormValues) => {
|
||||
const errors = {} as FormikErrors<FormikValues>;
|
||||
if (!values.name)
|
||||
|
@ -253,6 +291,13 @@ export function NotificationAddForm({ isOpen, toggle }: AddProps) {
|
|||
|
||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||
<div className="space-x-3 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
onClick={() => testNotification(values)}
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
|
@ -348,12 +393,27 @@ export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateP
|
|||
deleteMutation.mutate(notification.id);
|
||||
};
|
||||
|
||||
const testMutation = useMutation(
|
||||
(n: Notification) => APIClient.notifications.test(n),
|
||||
{
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const testNotification = (data: unknown) => {
|
||||
testMutation.mutate(data as Notification);
|
||||
};
|
||||
|
||||
const initialValues = {
|
||||
id: notification.id,
|
||||
enabled: notification.enabled,
|
||||
type: notification.type,
|
||||
name: notification.name,
|
||||
webhook: notification.webhook,
|
||||
token: notification.token,
|
||||
channel: notification.channel,
|
||||
events: notification.events || []
|
||||
};
|
||||
|
||||
|
@ -366,6 +426,7 @@ export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateP
|
|||
onSubmit={onSubmit}
|
||||
deleteAction={deleteAction}
|
||||
initialValues={initialValues}
|
||||
testFn={testNotification}
|
||||
>
|
||||
{(values) => (
|
||||
<div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useToggle } from "../../hooks/hooks";
|
|||
import { NotificationAddForm, NotificationUpdateForm } from "../../forms/settings/NotifiactionForms";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { classNames } from "../../utils";
|
||||
import { componentMapType } from "../../forms/settings/DownloadClientForms";
|
||||
|
||||
function NotificationSettings() {
|
||||
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false);
|
||||
|
@ -16,7 +17,7 @@ function NotificationSettings() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<div className="lg:col-span-9">
|
||||
<NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} />
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
|
@ -24,7 +25,7 @@ function NotificationSettings() {
|
|||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Notifications</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Send notifications on events.
|
||||
Send notifications on events.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
|
@ -33,7 +34,7 @@ function NotificationSettings() {
|
|||
onClick={toggleAddNotifications}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
Add new
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -42,10 +43,10 @@ function NotificationSettings() {
|
|||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full">
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
|
||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
|
||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
|
||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
|
||||
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
|
||||
</li>
|
||||
|
||||
{data && data.map((n: Notification) => (
|
||||
|
@ -59,6 +60,29 @@ function NotificationSettings() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
const DiscordIcon = () => (
|
||||
<svg viewBox="0 0 71 71" xmlns="http://www.w3.org/2000/svg" className="mr-2 h-4">
|
||||
<path
|
||||
d="M60.104 12.927a58.55 58.55 0 0 0-14.452-4.482.22.22 0 0 0-.232.11 40.783 40.783 0 0 0-1.8 3.696c-5.457-.817-10.886-.817-16.232 0-.484-1.164-1.2-2.586-1.827-3.696a.228.228 0 0 0-.233-.11 58.39 58.39 0 0 0-14.452 4.482.207.207 0 0 0-.095.082C1.577 26.759-.945 40.174.292 53.42a.244.244 0 0 0 .093.166c6.073 4.46 11.956 7.167 17.729 8.962a.23.23 0 0 0 .249-.082 42.08 42.08 0 0 0 3.627-5.9.225.225 0 0 0-.123-.312 38.772 38.772 0 0 1-5.539-2.64.228.228 0 0 1-.022-.377c.372-.28.744-.57 1.1-.862a.22.22 0 0 1 .23-.031c11.62 5.305 24.198 5.305 35.681 0a.219.219 0 0 1 .232.028c.356.293.728.586 1.103.865a.228.228 0 0 1-.02.377 36.384 36.384 0 0 1-5.54 2.637.227.227 0 0 0-.12.316 47.249 47.249 0 0 0 3.623 5.897.225.225 0 0 0 .25.084c5.8-1.795 11.683-4.502 17.756-8.962a.228.228 0 0 0 .093-.163c1.48-15.315-2.48-28.618-10.498-40.412a.18.18 0 0 0-.093-.085zM23.725 45.355c-3.498 0-6.38-3.212-6.38-7.156s2.826-7.156 6.38-7.156c3.582 0 6.437 3.24 6.38 7.156 0 3.944-2.826 7.156-6.38 7.156zm23.592 0c-3.498 0-6.38-3.212-6.38-7.156s2.826-7.156 6.38-7.156c3.582 0 6.437 3.24 6.38 7.156 0 3.944-2.798 7.156-6.38 7.156z"
|
||||
fill="currentColor"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const TelegramIcon = () => (
|
||||
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" className="mr-2 h-4">
|
||||
<path
|
||||
d="M0 24c0 13.255 10.745 24 24 24s24-10.745 24-24S37.255 0 24 0 0 10.745 0 24zm19.6 11 .408-6.118 11.129-10.043c.488-.433-.107-.645-.755-.252l-13.735 8.665-5.933-1.851c-1.28-.393-1.29-1.273.288-1.906l23.118-8.914c1.056-.48 2.075.254 1.672 1.87l-3.937 18.553c-.275 1.318-1.072 1.633-2.175 1.024l-5.998-4.43L20.8 34.4l-.027.027c-.323.314-.59.573-1.173.573z"
|
||||
clipRule="evenodd" fill="currentColor" fillRule="evenodd"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
const iconComponentMap: componentMapType = {
|
||||
DISCORD: <span className="flex items-center px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"><DiscordIcon /> Discord</span>,
|
||||
TELEGRAM: <span className="flex items-center px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"><TelegramIcon /> Telegram</span>
|
||||
};
|
||||
|
||||
interface ListItemProps {
|
||||
notification: Notification;
|
||||
}
|
||||
|
@ -70,8 +94,8 @@ function ListItem({ notification }: ListItemProps) {
|
|||
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
|
||||
<NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} />
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-3">
|
||||
<div className="col-span-2 flex items-center sm:px-6">
|
||||
<Switch
|
||||
checked={notification.enabled}
|
||||
onChange={toggleUpdateForm}
|
||||
|
@ -90,25 +114,23 @@ function ListItem({ notification }: ListItemProps) {
|
|||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
<div className="col-span-4 flex items-center sm:px-6">
|
||||
{notification.name}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.type}
|
||||
<div className="col-span-2 flex items-center sm:px-6">
|
||||
{iconComponentMap[notification.type]}
|
||||
</div>
|
||||
<div className="col-span-5 flex items-center sm:px-6 ">
|
||||
{notification.events.map((n, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
|
||||
>
|
||||
{n}
|
||||
</span>
|
||||
))}
|
||||
<div className="col-span-3 flex items-center sm:px-6">
|
||||
<span
|
||||
className="mr-2 inline-flex items-center px-2.5 py-1 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
|
||||
title={notification.events.join(", ")}
|
||||
>
|
||||
{notification.events.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||
<div className="col-span-1 flex items-center">
|
||||
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateForm}>
|
||||
Edit
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
6
web/src/types/Notification.d.ts
vendored
6
web/src/types/Notification.d.ts
vendored
|
@ -1,4 +1,4 @@
|
|||
type NotificationType = "DISCORD";
|
||||
type NotificationType = "DISCORD" | "TELEGRAM";
|
||||
|
||||
interface Notification {
|
||||
id: number;
|
||||
|
@ -6,5 +6,7 @@ interface Notification {
|
|||
enabled: boolean;
|
||||
type: NotificationType;
|
||||
events: string[];
|
||||
webhook: string;
|
||||
webhook?: string;
|
||||
token?: string;
|
||||
channel?: string;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue