Skip to content

Commit 5ea49f4

Browse files
authored
feat(notifications): filter notifications (#130)
* feat(notifications): filter notifications * fix(notifications): default slice not overwriting correctly * chore(logging): move notification skip logging to debug level * refactor: receivers and var names (#131) * perf(utils): optimize slice functions (#132) * perf(utils): optimize slice functions * test(utils): added tests for SimplifyHDRSlice * refactor(notifications): map notification level to status codes * fix(config): workaround for viper not overwriting slice
1 parent 78624fd commit 5ea49f4

File tree

8 files changed

+139
-24
lines changed

8 files changed

+139
-24
lines changed

config.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,21 @@ fuzzyMatching:
147147
# You can decide which notifications you want to receive
148148
#
149149
notifications:
150+
# Notification Level
151+
# Decides what notifications you want to receive
152+
#
153+
# Default: [ "MATCH", "ERROR" ]
154+
#
155+
# Options: "MATCH", "INFO", "ERROR"
156+
#
157+
# Examples:
158+
# [ "MATCH", "INFO", "ERROR" ] would send everything
159+
# [ "MATCH", "INFO" ] would send all matches and rejection infos
160+
# [ "MATCH", "ERROR" ] would send all matches and errors
161+
# [ "ERROR" ] would only send all errors
162+
#
163+
notificationLevel: [ "MATCH", "ERROR" ]
164+
150165
# Discord
151166
# Uses the given Discord webhook to send notifications for various events
152167
#

internal/config/config.go

+25-1
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,21 @@ fuzzyMatching:
174174
# You can decide which notifications you want to receive
175175
#
176176
notifications:
177+
# Notification Level
178+
# Decides what notifications you want to receive
179+
#
180+
# Default: [ "MATCH", "ERROR" ]
181+
#
182+
# Options: "MATCH", "INFO", "ERROR"
183+
#
184+
# Examples:
185+
# [ "MATCH", "INFO", "ERROR" ] would send everything
186+
# [ "MATCH", "INFO" ] would send all matches and rejection infos
187+
# [ "MATCH", "ERROR" ] would send all matches and errors
188+
# [ "ERROR" ] would only send all errors
189+
#
190+
notificationLevel: [ "MATCH", "ERROR" ]
191+
177192
# Discord
178193
# Uses the given Discord webhook to send notifications for various events
179194
#
@@ -314,7 +329,8 @@ func (c *AppConfig) defaults() {
314329
},
315330
APIToken: "",
316331
Notifications: domain.Notifications{
317-
Discord: "",
332+
NotificationLevel: []string{"MATCH", "ERROR"},
333+
Discord: "",
318334
// Notifiarr: "",
319335
// Shoutrrr: "",
320336
},
@@ -399,6 +415,11 @@ func (c *AppConfig) load(configPath string) {
399415
if err := viper.Unmarshal(c.Config); err != nil {
400416
log.Fatalf("Could not unmarshal config file: %v: err %q", viper.ConfigFileUsed(), err)
401417
}
418+
419+
// workaround for notificationLevel default slice not being overwritten properly by viper
420+
if levels := viper.GetStringSlice("notifications.notificationLevel"); len(levels) != 0 {
421+
c.Config.Notifications.NotificationLevel = levels
422+
}
402423
}
403424

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

445+
notificationLevel := viper.GetStringSlice("notifications.notificationLevel")
446+
c.Config.Notifications.NotificationLevel = notificationLevel
447+
424448
log.Debug().Msg("config file reloaded!")
425449

426450
c.m.Unlock()

internal/domain/config.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ type FuzzyMatching struct {
1818
}
1919

2020
type Notifications struct {
21-
Discord string `yaml:"discord"`
21+
NotificationLevel []string `yaml:"notificationLevel"`
22+
Discord string `yaml:"discord"`
2223
// Notifiarr string `yaml:"notifiarr"`
2324
// Shoutrrr string `yaml:"shoutrrr"`
2425
}

internal/domain/http.go

+33
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,36 @@ const (
3030
StatusGetEpisodesError = 464
3131
StatusEpisodeCountError = 450
3232
)
33+
34+
var StatusMap = map[string][]int{
35+
NotificationLevelMatch: {
36+
StatusSuccessfulMatch,
37+
},
38+
NotificationLevelInfo: {
39+
StatusNoMatches,
40+
StatusResolutionMismatch,
41+
StatusSourceMismatch,
42+
StatusRlsGrpMismatch,
43+
StatusCutMismatch,
44+
StatusEditionMismatch,
45+
StatusRepackStatusMismatch,
46+
StatusHdrMismatch,
47+
StatusStreamingServiceMismatch,
48+
StatusAlreadyInClient,
49+
StatusNotASeasonPack,
50+
StatusBelowThreshold,
51+
},
52+
NotificationLevelError: {
53+
StatusFailedHardlink,
54+
StatusClientNotFound,
55+
StatusGetClientError,
56+
StatusDecodingError,
57+
StatusAnnounceNameError,
58+
StatusGetTorrentsError,
59+
StatusTorrentBytesError,
60+
StatusDecodeTorrentBytesError,
61+
StatusParseTorrentInfoError,
62+
StatusGetEpisodesError,
63+
StatusEpisodeCountError,
64+
},
65+
}

internal/domain/notification.go

+7
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
package domain
66

77
type Sender interface {
8+
Name() string
89
Send(statusCode int, payload NotificationPayload) error
910
}
1011

12+
const (
13+
NotificationLevelInfo = "INFO"
14+
NotificationLevelError = "ERROR"
15+
NotificationLevelMatch = "MATCH"
16+
)
17+
1118
type NotificationPayload struct {
1219
Subject string
1320
Message string

internal/http/processor.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func (p *processor) ProcessSeasonPackHandler(w netHTTP.ResponseWriter, r *netHTT
176176
Action: "Pack",
177177
Error: err,
178178
}); sendErr != nil {
179-
p.log.Error().Err(sendErr).Msg("error sending notification")
179+
p.log.Error().Err(sendErr).Msgf("could not send %s notification for %d", p.noti.Name(), code)
180180
}
181181

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

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

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

432432
p.log.Info().Msg("successfully parsed torrent and hardlinked episodes")

internal/notification/discord.go

+44-18
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
package notification
66

77
import (
8+
"bufio"
89
"bytes"
910
"encoding/json"
1011
"fmt"
1112
"io"
1213
"net/http"
14+
"slices"
1315
"strings"
1416
"time"
1517

@@ -66,8 +68,18 @@ func NewDiscordSender(log logger.Logger, config *config.AppConfig) domain.Sender
6668
}
6769
}
6870

71+
func (s *discordSender) Name() string {
72+
return "discord"
73+
}
74+
6975
func (s *discordSender) Send(statusCode int, payload domain.NotificationPayload) error {
7076
if !s.isEnabled() {
77+
s.log.Debug().Msg("no webhook defined, skipping notification")
78+
return nil
79+
}
80+
81+
if !s.shouldSend(statusCode) {
82+
s.log.Debug().Msg("no notification wanted for this status, skipping notification")
7183
return nil
7284
}
7385

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

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

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

89101
req.Header.Set("Content-Type", "application/json")
90-
//req.Header.Set("User-Agent", "seasonpackarr")
102+
// req.Header.Set("User-Agent", "seasonpackarr")
91103

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

97109
defer res.Body.Close()
98110

99-
body, err := io.ReadAll(res.Body)
100-
if err != nil {
101-
return errors.Wrap(err, "discord client could not read data")
102-
}
103-
104-
s.log.Trace().Msgf("discord status: %v response: %v", res.StatusCode, string(body))
111+
s.log.Trace().Msgf("discord response status: %d", res.StatusCode)
105112

106113
// discord responds with 204, Notifiarr with 204 so lets take all 200 as ok
107-
if res.StatusCode >= 300 {
108-
return errors.New("bad discord client status: %v body: %v", res.StatusCode, string(body))
114+
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
115+
body, err := io.ReadAll(bufio.NewReader(res.Body))
116+
if err != nil {
117+
return errors.Wrap(err, "could not read body for status: %v payload: %v", statusCode, payload)
118+
}
119+
120+
return errors.New("unexpected status: %v body: %v", res.StatusCode, string(body))
109121
}
110122

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

116128
func (s *discordSender) isEnabled() bool {
117-
if s.cfg.Config.Notifications.Discord == "" {
118-
s.log.Warn().Msg("no webhook defined, skipping notification")
129+
return len(s.cfg.Config.Notifications.Discord) != 0
130+
}
131+
132+
func (s *discordSender) shouldSend(statusCode int) bool {
133+
if len(s.cfg.Config.Notifications.NotificationLevel) == 0 {
119134
return false
120135
}
121136

122-
return true
137+
statusCodes := make(map[int]struct{})
138+
139+
for _, level := range s.cfg.Config.Notifications.NotificationLevel {
140+
if codes, ok := domain.StatusMap[level]; ok {
141+
for _, code := range codes {
142+
statusCodes[code] = struct{}{}
143+
}
144+
}
145+
}
146+
147+
_, shouldSend := statusCodes[statusCode]
148+
return shouldSend
123149
}
124150

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

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

165191
if payload.Error != nil {
166192
// actual error?
167-
if statusCode >= 400 {
193+
if slices.Contains(domain.StatusMap[domain.NotificationLevelError], statusCode) {
168194
f := DiscordEmbedsFields{
169195
Name: "Error",
170196
Value: fmt.Sprintf("```%s```", payload.Error.Error()),

schemas/config-schema.json

+9
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@
110110
"type": "object",
111111
"additionalProperties": false,
112112
"properties": {
113+
"notificationLevel": {
114+
"type": "array",
115+
"items": {
116+
"type": "string",
117+
"enum": ["MATCH", "INFO", "ERROR"]
118+
},
119+
"minItems": 1,
120+
"uniqueItems": true
121+
},
113122
"discord": {
114123
"type": "string",
115124
"default": ""

0 commit comments

Comments
 (0)