diff --git a/internal/domain/notification.go b/internal/domain/notification.go index bd40a63..cb45006 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -80,6 +80,7 @@ const ( NotificationTypeSlack NotificationType = "SLACK" NotificationTypeTelegram NotificationType = "TELEGRAM" NotificationTypeGotify NotificationType = "GOTIFY" + NotificationTypeNtfy NotificationType = "NTFY" NotificationTypeLunaSea NotificationType = "LUNASEA" ) diff --git a/internal/http/auth_test.go b/internal/http/auth_test.go index 5eb930a..dae9ee5 100644 --- a/internal/http/auth_test.go +++ b/internal/http/auth_test.go @@ -91,8 +91,6 @@ func setupAuthHandler() { } func TestAuthHandlerLogin(t *testing.T) { - t.Parallel() - logger := zerolog.Nop() encoder := encoder{} cookieStore := sessions.NewCookieStore([]byte("test")) @@ -156,8 +154,6 @@ func TestAuthHandlerLogin(t *testing.T) { } func TestAuthHandlerValidateOK(t *testing.T) { - t.Parallel() - logger := zerolog.Nop() encoder := encoder{} cookieStore := sessions.NewCookieStore([]byte("test")) @@ -233,8 +229,6 @@ func TestAuthHandlerValidateOK(t *testing.T) { } func TestAuthHandlerValidateBad(t *testing.T) { - t.Parallel() - logger := zerolog.Nop() encoder := encoder{} cookieStore := sessions.NewCookieStore([]byte("test")) @@ -284,8 +278,6 @@ func TestAuthHandlerValidateBad(t *testing.T) { } func TestAuthHandlerLoginBad(t *testing.T) { - t.Parallel() - logger := zerolog.Nop() encoder := encoder{} cookieStore := sessions.NewCookieStore([]byte("test")) @@ -335,8 +327,6 @@ func TestAuthHandlerLoginBad(t *testing.T) { } func TestAuthHandlerLogout(t *testing.T) { - t.Parallel() - logger := zerolog.Nop() encoder := encoder{} cookieStore := sessions.NewCookieStore([]byte("test")) @@ -419,7 +409,7 @@ func TestAuthHandlerLogout(t *testing.T) { defer resp.Body.Close() if status := resp.StatusCode; status != http.StatusNoContent { - t.Errorf("validate: handler returned wrong status code: got %v want %v", status, http.StatusNoContent) + t.Errorf("logout: handler returned wrong status code: got %v want %v", status, http.StatusNoContent) } //if v := resp.Header.Get("Set-Cookie"); v != "" { diff --git a/internal/notification/message_builder.go b/internal/notification/message_builder.go index 16108c4..05354f5 100644 --- a/internal/notification/message_builder.go +++ b/internal/notification/message_builder.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/autobrr/autobrr/internal/domain" + "github.com/dustin/go-humanize" ) @@ -26,12 +27,11 @@ func (b *NotificationBuilderPlainText) BuildBody(payload domain.NotificationPayl buildPart(payload.Status != "", "\nStatus: %v", payload.Status.String()) buildPart(payload.Indexer != "", "\nIndexer: %v", payload.Indexer) buildPart(payload.Filter != "", "\nFilter: %v", payload.Filter) - buildPart(payload.Action != "", "\nAction: %v Type: %v", payload.Action, payload.ActionType) - buildPart(len(payload.Rejections) > 0, "\nRejections: %v", strings.Join(payload.Rejections, ", ")) - + buildPart(payload.Action != "", "\nAction: %v: %v", payload.ActionType, payload.Action) if payload.Action != "" && payload.ActionClient != "" { parts = append(parts, fmt.Sprintf(" Client: %v", payload.ActionClient)) } + buildPart(len(payload.Rejections) > 0, "\nRejections: %v", strings.Join(payload.Rejections, ", ")) return strings.Join(parts, "\n") } @@ -42,7 +42,7 @@ func (b *NotificationBuilderPlainText) BuildTitle(event domain.NotificationEvent domain.NotificationEventAppUpdateAvailable: "Autobrr update available", domain.NotificationEventPushApproved: "Push Approved", domain.NotificationEventPushRejected: "Push Rejected", - domain.NotificationEventPushError: "Error", + domain.NotificationEventPushError: "Push Error", domain.NotificationEventIRCDisconnected: "IRC Disconnected", domain.NotificationEventIRCReconnected: "IRC Reconnected", domain.NotificationEventTest: "Test", diff --git a/internal/notification/ntfy.go b/internal/notification/ntfy.go new file mode 100644 index 0000000..98aa5ee --- /dev/null +++ b/internal/notification/ntfy.go @@ -0,0 +1,131 @@ +// Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors. +// SPDX-License-Identifier: GPL-2.0-or-later + +package notification + +import ( + "io" + "net/http" + "strconv" + "strings" + "time" + + "github.com/autobrr/autobrr/internal/domain" + "github.com/autobrr/autobrr/pkg/errors" + "github.com/autobrr/autobrr/pkg/sharedhttp" + + "github.com/rs/zerolog" +) + +type ntfyMessage struct { + Message string `json:"message"` + Title string `json:"title"` +} + +type ntfySender struct { + log zerolog.Logger + Settings domain.Notification + builder NotificationBuilderPlainText + + httpClient *http.Client +} + +func NewNtfySender(log zerolog.Logger, settings domain.Notification) domain.NotificationSender { + return &ntfySender{ + log: log.With().Str("sender", "ntfy").Logger(), + Settings: settings, + builder: NotificationBuilderPlainText{}, + httpClient: &http.Client{ + Timeout: time.Second * 30, + Transport: sharedhttp.Transport, + }, + } +} + +func (s *ntfySender) Send(event domain.NotificationEvent, payload domain.NotificationPayload) error { + m := ntfyMessage{ + Message: s.builder.BuildBody(payload), + Title: s.builder.BuildTitle(event), + } + + req, err := http.NewRequest(http.MethodPost, s.Settings.Host, strings.NewReader(m.Message)) + if err != nil { + s.log.Error().Err(err).Msgf("ntfy client request error: %v", event) + return errors.Wrap(err, "could not create request") + } + + req.Header.Set("Content-Type", "text/pain") + req.Header.Set("User-Agent", "autobrr") + + req.Header.Set("Title", m.Title) + if s.Settings.Priority > 0 { + req.Header.Set("Priority", strconv.Itoa(int(s.Settings.Priority))) + } + + // set basic auth or access token + if s.Settings.Username != "" && s.Settings.Password != "" { + req.SetBasicAuth(s.Settings.Username, s.Settings.Password) + } else if s.Settings.Token != "" { + req.Header.Set("Authorization", "Bearer "+s.Settings.Token) + } + + res, err := s.httpClient.Do(req) + if err != nil { + s.log.Error().Err(err).Msgf("ntfy client request error: %v", event) + return errors.Wrap(err, "could not make request: %+v", req) + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + s.log.Error().Err(err).Msgf("ntfy client request error: %v", event) + return errors.Wrap(err, "could not read data") + } + + s.log.Trace().Msgf("ntfy status: %v response: %v", res.StatusCode, string(body)) + + if res.StatusCode != http.StatusOK { + s.log.Error().Err(err).Msgf("ntfy client request error: %v", string(body)) + return errors.New("bad status: %v body: %v", res.StatusCode, string(body)) + } + + s.log.Debug().Msg("notification successfully sent to ntfy") + + return nil +} + +func (s *ntfySender) CanSend(event domain.NotificationEvent) bool { + if s.isEnabled() && s.isEnabledEvent(event) { + return true + } + return false +} + +func (s *ntfySender) isEnabled() bool { + if s.Settings.Enabled { + if s.Settings.Host == "" { + s.log.Warn().Msg("ntfy missing host") + return false + } + + if s.Settings.Token == "" { + s.log.Warn().Msg("ntfy missing application token") + return false + } + + return true + } + + return false +} + +func (s *ntfySender) isEnabledEvent(event domain.NotificationEvent) bool { + for _, e := range s.Settings.Events { + if e == string(event) { + return true + } + } + + return false +} diff --git a/internal/notification/service.go b/internal/notification/service.go index acd2331..56649b2 100644 --- a/internal/notification/service.go +++ b/internal/notification/service.go @@ -249,6 +249,8 @@ func (s *service) Test(ctx context.Context, notification domain.Notification) er agent = NewPushoverSender(s.log, notification) case domain.NotificationTypeGotify: agent = NewGotifySender(s.log, notification) + case domain.NotificationTypeNtfy: + agent = NewNtfySender(s.log, notification) case domain.NotificationTypeLunaSea: agent = NewLunaSeaSender(s.log, notification) default: diff --git a/web/src/domain/constants.ts b/web/src/domain/constants.ts index b6dbaa4..990e92b 100644 --- a/web/src/domain/constants.ts +++ b/web/src/domain/constants.ts @@ -406,6 +406,10 @@ export const NotificationTypeOptions: OptionBasicTyped[] = [ label: "Gotify", value: "GOTIFY" }, + { + label: "Ntfy", + value: "NTFY" + }, { label: "LunaSea", value: "LUNASEA" diff --git a/web/src/forms/settings/NotificationForms.tsx b/web/src/forms/settings/NotificationForms.tsx index 27a80c6..a2aaa37 100644 --- a/web/src/forms/settings/NotificationForms.tsx +++ b/web/src/forms/settings/NotificationForms.tsx @@ -197,12 +197,55 @@ function FormFieldsGotify() { ); } +function FormFieldsNtfy() { + return ( +
+
+ Settings +
+ + + + + + + + + + +
+ ); +} + const componentMap: componentMapType = { DISCORD: , NOTIFIARR: , TELEGRAM: , PUSHOVER: , GOTIFY: , + NTFY: , LUNASEA: }; diff --git a/web/src/screens/settings/Notifications.tsx b/web/src/screens/settings/Notifications.tsx index dce8d49..cd4007e 100644 --- a/web/src/screens/settings/Notifications.tsx +++ b/web/src/screens/settings/Notifications.tsx @@ -15,7 +15,7 @@ import toast from "react-hot-toast"; import { Section } from "./_components"; import { PlusIcon } from "@heroicons/react/24/solid"; import { Checkbox } from "@components/Checkbox"; -import { DiscordIcon, GotifyIcon, LunaSeaIcon, NotifiarrIcon, PushoverIcon, TelegramIcon } from "./_components"; +import { DiscordIcon, GotifyIcon, LunaSeaIcon, NotifiarrIcon, NtfyIcon, PushoverIcon, TelegramIcon } from "./_components"; export const notificationKeys = { all: ["notifications"] as const, @@ -76,6 +76,7 @@ const iconComponentMap: componentMapType = { TELEGRAM: Telegram, PUSHOVER: Pushover, GOTIFY: Gotify, + NTFY: ntfy, LUNASEA: LunaSea }; diff --git a/web/src/screens/settings/_components.tsx b/web/src/screens/settings/_components.tsx index 6aae69a..4911b0b 100644 --- a/web/src/screens/settings/_components.tsx +++ b/web/src/screens/settings/_components.tsx @@ -127,6 +127,14 @@ export const GotifyIcon = () => ( ); +export const NtfyIcon = () => ( + + + + + +); + export const LunaSeaIcon = () => ( diff --git a/web/src/types/Notification.d.ts b/web/src/types/Notification.d.ts index 0bf0469..0384937 100644 --- a/web/src/types/Notification.d.ts +++ b/web/src/types/Notification.d.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -type NotificationType = "DISCORD" | "NOTIFIARR" | "TELEGRAM" | "PUSHOVER" | "GOTIFY" | "LUNASEA"; +type NotificationType = "DISCORD" | "NOTIFIARR" | "TELEGRAM" | "PUSHOVER" | "GOTIFY" | "NTFY" | "LUNASEA"; type NotificationEvent = "PUSH_APPROVED" | "PUSH_REJECTED"