mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(notifications): add ntfy support (#1323)
* feat(notifications): add ntfy support * fix(test): update * fix: added missing semicolon
This commit is contained in:
parent
3234f0d919
commit
3dd1629a3f
10 changed files with 197 additions and 17 deletions
|
@ -80,6 +80,7 @@ const (
|
|||
NotificationTypeSlack NotificationType = "SLACK"
|
||||
NotificationTypeTelegram NotificationType = "TELEGRAM"
|
||||
NotificationTypeGotify NotificationType = "GOTIFY"
|
||||
NotificationTypeNtfy NotificationType = "NTFY"
|
||||
NotificationTypeLunaSea NotificationType = "LUNASEA"
|
||||
)
|
||||
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
|
@ -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",
|
||||
|
|
131
internal/notification/ntfy.go
Normal file
131
internal/notification/ntfy.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -406,6 +406,10 @@ export const NotificationTypeOptions: OptionBasicTyped<NotificationType>[] = [
|
|||
label: "Gotify",
|
||||
value: "GOTIFY"
|
||||
},
|
||||
{
|
||||
label: "Ntfy",
|
||||
value: "NTFY"
|
||||
},
|
||||
{
|
||||
label: "LunaSea",
|
||||
value: "LUNASEA"
|
||||
|
|
|
@ -197,12 +197,55 @@ function FormFieldsGotify() {
|
|||
);
|
||||
}
|
||||
|
||||
function FormFieldsNtfy() {
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-4">
|
||||
<div className="px-4 space-y-1">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Settings</Dialog.Title>
|
||||
</div>
|
||||
|
||||
<TextFieldWide
|
||||
name="host"
|
||||
label="NTFY URL"
|
||||
help="NTFY URL"
|
||||
placeholder="https://ntfy.sh/mytopic"
|
||||
required={true}
|
||||
/>
|
||||
|
||||
<TextFieldWide
|
||||
name="username"
|
||||
label="Username"
|
||||
help="Username"
|
||||
/>
|
||||
|
||||
<PasswordFieldWide
|
||||
name="password"
|
||||
label="Password"
|
||||
help="Password"
|
||||
/>
|
||||
|
||||
<PasswordFieldWide
|
||||
name="token"
|
||||
label="Access token"
|
||||
help="Access token. Use this or Usernmae+password"
|
||||
/>
|
||||
|
||||
<NumberFieldWide
|
||||
name="priority"
|
||||
label="Priority"
|
||||
help="Max 5, 4, 3 (default), 2, 1 Min"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const componentMap: componentMapType = {
|
||||
DISCORD: <FormFieldsDiscord />,
|
||||
NOTIFIARR: <FormFieldsNotifiarr />,
|
||||
TELEGRAM: <FormFieldsTelegram />,
|
||||
PUSHOVER: <FormFieldsPushover />,
|
||||
GOTIFY: <FormFieldsGotify />,
|
||||
NTFY: <FormFieldsNtfy />,
|
||||
LUNASEA: <FormFieldsLunaSea />
|
||||
};
|
||||
|
||||
|
|
|
@ -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: <span className={iconStyle}><TelegramIcon /> Telegram</span>,
|
||||
PUSHOVER: <span className={iconStyle}><PushoverIcon /> Pushover</span>,
|
||||
GOTIFY: <span className={iconStyle}><GotifyIcon /> Gotify</span>,
|
||||
NTFY: <span className={iconStyle}><NtfyIcon /> ntfy</span>,
|
||||
LUNASEA: <span className={iconStyle}><LunaSeaIcon /> LunaSea</span>
|
||||
};
|
||||
|
||||
|
|
|
@ -127,6 +127,14 @@ export const GotifyIcon = () => (
|
|||
</svg>
|
||||
);
|
||||
|
||||
export const NtfyIcon = () => (
|
||||
<svg {...commonSVGProps} viewBox="0 0 50.8 50.8" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M44.98 39.952V10.848H7.407v27.814l-1.587 4.2 8.393-2.91Z" />
|
||||
<path d="M27.781 31.485h8.202" />
|
||||
<path d="m65.979 100.011 9.511 5.492-9.511 5.491" transform="translate(-51.81 -80.758)"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LunaSeaIcon = () => (
|
||||
<svg {...commonSVGProps} viewBox="0 0 750 750">
|
||||
<path d="m554.69 180.46c-333.63 0-452.75 389.23-556.05 389.23 185.37 0 237.85-247.18 419.12-247.18l47.24-102.05z"/>
|
||||
|
|
2
web/src/types/Notification.d.ts
vendored
2
web/src/types/Notification.d.ts
vendored
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue