Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(notifications): filter notifications #130

Merged
merged 8 commits into from
Sep 4, 2024
15 changes: 15 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,21 @@ fuzzyMatching:
# You can decide which notifications you want to receive
#
notifications:
# Notification Level
# Decides what notifications you want to receive
#
# Default: [ "MATCH", "ERROR" ]
#
# Options: "MATCH", "INFO", "ERROR"
#
# Examples:
# [ "MATCH", "INFO", "ERROR" ] would send everything
# [ "MATCH", "INFO" ] would send all matches and rejection infos
# [ "MATCH", "ERROR" ] would send all matches and errors
# [ "ERROR" ] would only send all errors
#
notificationLevel: [ "MATCH", "ERROR" ]

# Discord
# Uses the given Discord webhook to send notifications for various events
#
Expand Down
26 changes: 25 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,21 @@ fuzzyMatching:
# You can decide which notifications you want to receive
#
notifications:
# Notification Level
# Decides what notifications you want to receive
#
# Default: [ "MATCH", "ERROR" ]
#
# Options: "MATCH", "INFO", "ERROR"
#
# Examples:
# [ "MATCH", "INFO", "ERROR" ] would send everything
# [ "MATCH", "INFO" ] would send all matches and rejection infos
# [ "MATCH", "ERROR" ] would send all matches and errors
# [ "ERROR" ] would only send all errors
#
notificationLevel: [ "MATCH", "ERROR" ]

# Discord
# Uses the given Discord webhook to send notifications for various events
#
Expand Down Expand Up @@ -314,7 +329,8 @@ func (c *AppConfig) defaults() {
},
APIToken: "",
Notifications: domain.Notifications{
Discord: "",
NotificationLevel: []string{"MATCH", "ERROR"},
Discord: "",
// Notifiarr: "",
// Shoutrrr: "",
},
Expand Down Expand Up @@ -399,6 +415,11 @@ func (c *AppConfig) load(configPath string) {
if err := viper.Unmarshal(c.Config); err != nil {
log.Fatalf("Could not unmarshal config file: %v: err %q", viper.ConfigFileUsed(), err)
}

// workaround for notificationLevel default slice not being overwritten properly by viper
if levels := viper.GetStringSlice("notifications.notificationLevel"); len(levels) != 0 {
c.Config.Notifications.NotificationLevel = levels
}
}

func (c *AppConfig) DynamicReload(log logger.Logger) {
Expand All @@ -421,6 +442,9 @@ func (c *AppConfig) DynamicReload(log logger.Logger) {
parseTorrentFile := viper.GetBool("parseTorrentFile")
c.Config.ParseTorrentFile = parseTorrentFile

notificationLevel := viper.GetStringSlice("notifications.notificationLevel")
c.Config.Notifications.NotificationLevel = notificationLevel

log.Debug().Msg("config file reloaded!")

c.m.Unlock()
Expand Down
3 changes: 2 additions & 1 deletion internal/domain/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ type FuzzyMatching struct {
}

type Notifications struct {
Discord string `yaml:"discord"`
NotificationLevel []string `yaml:"notificationLevel"`
Discord string `yaml:"discord"`
// Notifiarr string `yaml:"notifiarr"`
// Shoutrrr string `yaml:"shoutrrr"`
}
Expand Down
33 changes: 33 additions & 0 deletions internal/domain/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,36 @@ const (
StatusGetEpisodesError = 464
StatusEpisodeCountError = 450
)

var StatusMap = map[string][]int{
NotificationLevelMatch: {
StatusSuccessfulMatch,
},
NotificationLevelInfo: {
StatusNoMatches,
StatusResolutionMismatch,
StatusSourceMismatch,
StatusRlsGrpMismatch,
StatusCutMismatch,
StatusEditionMismatch,
StatusRepackStatusMismatch,
StatusHdrMismatch,
StatusStreamingServiceMismatch,
StatusAlreadyInClient,
StatusNotASeasonPack,
StatusBelowThreshold,
},
NotificationLevelError: {
StatusFailedHardlink,
StatusClientNotFound,
StatusGetClientError,
StatusDecodingError,
StatusAnnounceNameError,
StatusGetTorrentsError,
StatusTorrentBytesError,
StatusDecodeTorrentBytesError,
StatusParseTorrentInfoError,
StatusGetEpisodesError,
StatusEpisodeCountError,
},
}
7 changes: 7 additions & 0 deletions internal/domain/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@
package domain

type Sender interface {
Name() string
Send(statusCode int, payload NotificationPayload) error
}

const (
NotificationLevelInfo = "INFO"
NotificationLevelError = "ERROR"
NotificationLevelMatch = "MATCH"
)

type NotificationPayload struct {
Subject string
Message string
Expand Down
8 changes: 4 additions & 4 deletions internal/http/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (p *processor) ProcessSeasonPackHandler(w netHTTP.ResponseWriter, r *netHTT
Action: "Pack",
Error: err,
}); sendErr != nil {
p.log.Error().Err(sendErr).Msg("error sending notification")
p.log.Error().Err(sendErr).Msgf("could not send %s notification for %d", p.noti.Name(), code)
}

p.log.Error().Err(err).Msgf("error processing season pack: %d", code)
Expand All @@ -189,7 +189,7 @@ func (p *processor) ProcessSeasonPackHandler(w netHTTP.ResponseWriter, r *netHTT
Client: p.req.ClientName,
Action: "Pack",
}); sendErr != nil {
p.log.Error().Err(sendErr).Msg("error sending notification")
p.log.Error().Err(sendErr).Msgf("could not send %s notification for %d", p.noti.Name(), code)
}

p.log.Info().Msg("successfully matched season pack to episodes in client")
Expand Down Expand Up @@ -413,7 +413,7 @@ func (p *processor) ParseTorrentHandler(w netHTTP.ResponseWriter, r *netHTTP.Req
Action: "Parse",
Error: err,
}); sendErr != nil {
p.log.Error().Err(sendErr).Msg("error sending notification")
p.log.Error().Err(sendErr).Msgf("could not send %s notification for %d", p.noti.Name(), code)
}

p.log.Error().Err(err).Msgf("error parsing torrent: %d", code)
Expand All @@ -426,7 +426,7 @@ func (p *processor) ParseTorrentHandler(w netHTTP.ResponseWriter, r *netHTTP.Req
Client: p.req.ClientName,
Action: "Parse",
}); sendErr != nil {
p.log.Error().Err(sendErr).Msg("error sending notification")
p.log.Error().Err(sendErr).Msgf("could not send %s notification for %d", p.noti.Name(), code)
}

p.log.Info().Msg("successfully parsed torrent and hardlinked episodes")
Expand Down
62 changes: 44 additions & 18 deletions internal/notification/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
package notification

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -66,8 +68,18 @@ func NewDiscordSender(log logger.Logger, config *config.AppConfig) domain.Sender
}
}

func (s *discordSender) Name() string {
return "discord"
}

func (s *discordSender) Send(statusCode int, payload domain.NotificationPayload) error {
if !s.isEnabled() {
s.log.Debug().Msg("no webhook defined, skipping notification")
return nil
}

if !s.shouldSend(statusCode) {
s.log.Debug().Msg("no notification wanted for this status, skipping notification")
return nil
}

Expand All @@ -78,34 +90,34 @@ func (s *discordSender) Send(statusCode int, payload domain.NotificationPayload)

jsonData, err := json.Marshal(m)
if err != nil {
return errors.Wrap(err, "discord client could not marshal data: %+v", m)
return errors.Wrap(err, "could not marshal json request for status: %v payload: %v", statusCode, payload)
}

req, err := http.NewRequest(http.MethodPost, s.cfg.Config.Notifications.Discord, bytes.NewBuffer(jsonData))
if err != nil {
return errors.Wrap(err, "discord client could not create request")
return errors.Wrap(err, "could not create request for status: %v payload: %v", statusCode, payload)
}

req.Header.Set("Content-Type", "application/json")
//req.Header.Set("User-Agent", "seasonpackarr")
// req.Header.Set("User-Agent", "seasonpackarr")

res, err := s.httpClient.Do(req)
if err != nil {
return errors.Wrap(err, "discord client could not make request: %+v", req)
return errors.Wrap(err, "client request error for status: %v payload: %v", statusCode, payload)
}

defer res.Body.Close()

body, err := io.ReadAll(res.Body)
if err != nil {
return errors.Wrap(err, "discord client could not read data")
}

s.log.Trace().Msgf("discord status: %v response: %v", res.StatusCode, string(body))
s.log.Trace().Msgf("discord response status: %d", res.StatusCode)

// discord responds with 204, Notifiarr with 204 so lets take all 200 as ok
if res.StatusCode >= 300 {
return errors.New("bad discord client status: %v body: %v", res.StatusCode, string(body))
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
body, err := io.ReadAll(bufio.NewReader(res.Body))
if err != nil {
return errors.Wrap(err, "could not read body for status: %v payload: %v", statusCode, payload)
}

return errors.New("unexpected status: %v body: %v", res.StatusCode, string(body))
}

s.log.Debug().Msg("notification successfully sent to discord")
Expand All @@ -114,20 +126,34 @@ func (s *discordSender) Send(statusCode int, payload domain.NotificationPayload)
}

func (s *discordSender) isEnabled() bool {
if s.cfg.Config.Notifications.Discord == "" {
s.log.Warn().Msg("no webhook defined, skipping notification")
return len(s.cfg.Config.Notifications.Discord) != 0
}

func (s *discordSender) shouldSend(statusCode int) bool {
if len(s.cfg.Config.Notifications.NotificationLevel) == 0 {
return false
}

return true
statusCodes := make(map[int]struct{})

for _, level := range s.cfg.Config.Notifications.NotificationLevel {
if codes, ok := domain.StatusMap[level]; ok {
for _, code := range codes {
statusCodes[code] = struct{}{}
}
}
}

_, shouldSend := statusCodes[statusCode]
return shouldSend
}

func (s *discordSender) buildEmbed(statusCode int, payload domain.NotificationPayload) DiscordEmbeds {
color := LIGHT_BLUE

if (statusCode >= 200) && (statusCode < 250) { // not matching
if slices.Contains(domain.StatusMap[domain.NotificationLevelInfo], statusCode) { // not matching
color = GRAY
} else if (statusCode >= 400) && (statusCode < 500) { // error processing
} else if slices.Contains(domain.StatusMap[domain.NotificationLevelError], statusCode) { // error processing
color = RED
} else { // success
color = GREEN
Expand Down Expand Up @@ -164,7 +190,7 @@ func (s *discordSender) buildEmbed(statusCode int, payload domain.NotificationPa

if payload.Error != nil {
// actual error?
if statusCode >= 400 {
if slices.Contains(domain.StatusMap[domain.NotificationLevelError], statusCode) {
f := DiscordEmbedsFields{
Name: "Error",
Value: fmt.Sprintf("```%s```", payload.Error.Error()),
Expand Down
9 changes: 9 additions & 0 deletions schemas/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@
"type": "object",
"additionalProperties": false,
"properties": {
"notificationLevel": {
"type": "array",
"items": {
"type": "string",
"enum": ["MATCH", "INFO", "ERROR"]
},
"minItems": 1,
"uniqueItems": true
},
"discord": {
"type": "string",
"default": ""
Expand Down
Loading