Skip to content

Commit 349535f

Browse files
committed
Merge branch 'feat/notifyv2' into dev
2 parents 9dbcc75 + 2db67b8 commit 349535f

4 files changed

Lines changed: 242 additions & 11 deletions

File tree

src/logging/logging.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ import (
77
)
88

99

10-
func Init(level string) {
11-
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
10+
func Init(level string, notifyClient NotificationClient) {
11+
baseHandler := slog.NewTextHandler( os.Stdout, &slog.HandlerOptions{
1212
Level: getLogLevel(level),
13+
AddSource: true,
1314
})
15+
16+
handler := &notifyHandler{
17+
handler: baseHandler,
18+
notify: notifyClient,
19+
}
1420
logger := slog.New(handler)
1521
slog.SetDefault(logger)
1622
}

src/logging/notify.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package logging
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"explo/src/config"
7+
"fmt"
8+
"log/slog"
9+
"net/http"
10+
"strings"
11+
12+
"github.com/nikoksr/notify"
13+
"github.com/nikoksr/notify/service/discord"
14+
nhttp "github.com/nikoksr/notify/service/http"
15+
"github.com/nikoksr/notify/service/matrix"
16+
"maunium.net/go/mautrix/id"
17+
)
18+
19+
// TODO: reuse notifier instead of creating a new one every time. right now it's fine cause Explo sends 1 message per run
20+
21+
22+
type NotificationClient struct {
23+
Cfg config.NotifyConfig
24+
}
25+
26+
func InitNotify(cfg config.NotifyConfig) NotificationClient {
27+
return NotificationClient{
28+
Cfg: cfg,
29+
}
30+
}
31+
32+
func sendMatrix(cfg config.MatrixNotif, msg string) error {
33+
// UserID and RoomID need to be cast as specific types
34+
srvc, err := matrix.New(id.UserID(cfg.UserID), id.RoomID(cfg.RoomID), cfg.HomeServer, cfg.AccessToken)
35+
if err != nil {
36+
return fmt.Errorf("failed to create new Matrix notification service: %s", err.Error())
37+
}
38+
39+
notifier := notify.New()
40+
notifier.UseServices(srvc)
41+
42+
err = notifier.Send(context.Background(), "Explo", msg)
43+
if err != nil {
44+
return err
45+
}
46+
47+
return nil
48+
}
49+
50+
/* discordgo module (which notify uses) doesn't handle errors correctly
51+
no errors are given even when authentication fails*/
52+
func sendDiscord(cfg config.DiscordNotif, msg string) error {
53+
srvc := discord.New()
54+
if err := srvc.AuthenticateWithBotToken(cfg.BotToken); err != nil {
55+
return fmt.Errorf("failed to authenticate against Discord: %s", err.Error())
56+
}
57+
58+
srvc.AddReceivers(cfg.ChannelIDs...)
59+
60+
notifier := notify.New()
61+
notifier.UseServices(srvc)
62+
63+
if err := notifier.Send(context.Background(), "**Explo**", msg); err != nil {
64+
return fmt.Errorf("failed to send Discord notification: %s", err.Error())
65+
}
66+
67+
return nil
68+
}
69+
70+
func sendHttp(cfg config.HttpNotif, msg string) error {
71+
httpNotify := nhttp.New()
72+
webhooks := getNotifWebhooks(cfg.ReceiverURLs)
73+
74+
httpNotify.AddReceivers(webhooks...)
75+
notifier := notify.NewWithServices(httpNotify)
76+
77+
if err := notifier.Send(context.Background(), "", msg); err != nil {
78+
return fmt.Errorf("failed to send HTTP notification: %s", err.Error())
79+
}
80+
return nil
81+
}
82+
83+
func getNotifWebhooks(urls []string) []*nhttp.Webhook{
84+
var webhooks []*nhttp.Webhook
85+
86+
for _, url := range urls {
87+
88+
webhooks = append(webhooks, &nhttp.Webhook{
89+
ContentType: "application/json",
90+
URL: url,
91+
Method: http.MethodPost,
92+
BuildPayload: func(subject, message string) (payload any) {
93+
payl := json.RawMessage(message)
94+
return payl
95+
},
96+
97+
})
98+
}
99+
return webhooks
100+
}
101+
102+
// format notification for message client
103+
func formatRecordMsgClient(n Notification) string {
104+
attrs := make([]string, 0, len(n.Attrs))
105+
106+
for k, v := range n.Attrs {
107+
attrs = append(attrs, fmt.Sprintf("%s=%v", k, v))
108+
}
109+
return fmt.Sprintf(
110+
"[%s] %s\n%s",
111+
n.Level,
112+
n.Message,
113+
strings.Join(attrs, ", "),
114+
)
115+
}
116+
117+
func formatRecordJSON(n Notification) string {
118+
nJSON, err := json.Marshal(n)
119+
if err != nil {
120+
slog.Error("failed to marshal notification", "err", err)
121+
}
122+
return string(nJSON)
123+
}
124+
125+
126+
func (c NotificationClient) SendNotification(n Notification) {
127+
var err error
128+
switch c.Cfg.Service {
129+
case "matrix":
130+
msg := formatRecordMsgClient(n)
131+
err = sendMatrix(c.Cfg.Matrix, msg)
132+
133+
case "discord":
134+
msg := formatRecordMsgClient(n)
135+
err = sendDiscord(c.Cfg.Discord, msg)
136+
137+
case "http":
138+
msg := formatRecordJSON(n)
139+
err = sendHttp(c.Cfg.Http, msg)
140+
141+
case "": // no system defined
142+
return
143+
default:
144+
err = fmt.Errorf("wrong system defined for notifications: %s", c.Cfg.Service)
145+
}
146+
if err != nil {
147+
slog.Error(err.Error())
148+
} else {
149+
slog.Info("notification sent", "service", c.Cfg.Service)
150+
}
151+
}

src/logging/notify_handler.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package logging
2+
3+
import (
4+
"log/slog"
5+
"context"
6+
"time"
7+
)
8+
// slog handler that checks whether to send notifications
9+
10+
type notifyHandler struct {
11+
handler slog.Handler
12+
notify NotificationClient
13+
}
14+
15+
type Notification struct {
16+
Time time.Time `json:"time"`
17+
Level string `json:"level"`
18+
Message string `json:"message"`
19+
Attrs map[string]any `json:"attributes"`
20+
21+
}
22+
23+
func (h *notifyHandler) Enabled(ctx context.Context, level slog.Level) bool {
24+
return h.handler.Enabled(ctx, level)
25+
}
26+
27+
func (h *notifyHandler) Handle(ctx context.Context, r slog.Record) error {
28+
if shouldNotify(r) {
29+
// send notification in another goroutine
30+
notifyStruct := recordToStruct(r)
31+
go h.notify.SendNotification(notifyStruct)
32+
}
33+
return h.handler.Handle(ctx, r)
34+
}
35+
36+
func (h *notifyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
37+
return &notifyHandler{
38+
handler: h.handler.WithAttrs(attrs),
39+
notify: h.notify,
40+
}
41+
}
42+
43+
func (h *notifyHandler) WithGroup(name string) slog.Handler {
44+
return &notifyHandler{
45+
handler: h.handler.WithGroup(name),
46+
notify: h.notify,
47+
}
48+
}
49+
50+
func shouldNotify(r slog.Record) bool {
51+
notify := false
52+
53+
r.Attrs(func(a slog.Attr) bool {
54+
if a.Key == "notify" && a.Value.Kind() == slog.KindBool && a.Value.Bool() {
55+
notify = true
56+
return false // stop scanning
57+
}
58+
return true
59+
})
60+
61+
return notify
62+
}
63+
64+
func recordToStruct(r slog.Record) Notification {
65+
attrs := make(map[string]any, r.NumAttrs())
66+
67+
r.Attrs(func(a slog.Attr) bool {
68+
// filter out notify control key
69+
if a.Key != "notify" {
70+
attrs[a.Key] = a.Value.Any()
71+
}
72+
return true
73+
})
74+
75+
return Notification{
76+
Time: r.Time,
77+
Level: r.Level.String(),
78+
Message: r.Message,
79+
Attrs: attrs,
80+
}
81+
}

src/main/main.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"explo/src/logging"
5-
"fmt"
65
"log"
76
"log/slog"
87
"os"
@@ -29,7 +28,8 @@ func initHttpClient() *util.HttpClient {
2928
// Inits debug, gets playlist name, if needed, handles deprecation
3029
func setup(cfg *config.Config) {
3130
cfg.HandleDeprecation()
32-
logging.Init(cfg.LogLevel)
31+
notifyClient := logging.InitNotify(cfg.NotifyCfg)
32+
logging.Init(cfg.LogLevel, notifyClient)
3333
cfg.GenPlaylistName()
3434
}
3535

@@ -44,25 +44,21 @@ func main() {
4444
slog.Info("Starting Explo...")
4545

4646
httpClient := initHttpClient()
47-
notifyClient := logging.InitNotify(cfg.NotifyCfg)
4847
client, err := client.NewClient(&cfg)
4948
if err != nil {
5049
slog.Error(err.Error())
51-
notifyClient.SendNotification(err.Error())
5250
os.Exit(1)
5351
}
5452
discovery := discovery.NewDiscoverer(cfg.DiscoveryCfg, httpClient)
5553
downloader, err := downloader.NewDownloader(&cfg.DownloadCfg, httpClient, cfg.Flags.ExcludeLocal)
5654
if err != nil {
5755
slog.Error(err.Error())
58-
notifyClient.SendNotification(err.Error())
5956
os.Exit(1)
6057
}
6158

6259
tracks, err := discovery.Discover()
6360
if err != nil {
6461
slog.Error(err.Error())
65-
notifyClient.SendNotification(err.Error())
6662
os.Exit(1)
6763
}
6864
if !cfg.Persist {
@@ -84,16 +80,13 @@ func main() {
8480
downloader.StartDownload(&tracks)
8581
if len(tracks) == 0 {
8682
slog.Error("couldn't download any tracks")
87-
notifyClient.SendNotification("couldn't download any tracks")
8883
os.Exit(1)
8984
}
9085
}
9186

9287
if err := client.CreatePlaylist(tracks); err != nil {
9388
slog.Warn(err.Error())
94-
notifyClient.SendNotification(err.Error())
9589
} else {
9690
slog.Info("playlist created successfully", "system", cfg.System, "name", cfg.ClientCfg.PlaylistName)
97-
notifyClient.SendNotification(fmt.Sprintf("%s playlist created in %s", cfg.ClientCfg.PlaylistName, cfg.System))
9891
}
9992
}

0 commit comments

Comments
 (0)