diff --git a/config.go b/config.go index 56b4f1f..f1aa3a1 100644 --- a/config.go +++ b/config.go @@ -258,9 +258,15 @@ type configActivityPub struct { } type configNotifications struct { + Ntfy *configNtfy `mapstructure:"ntfy"` Telegram *configTelegram `mapstructure:"telegram"` } +type configNtfy struct { + Enabled bool `mapstructure:"enabled"` + Topic string `mapstructure:"topic"` +} + type configTelegram struct { Enabled bool `mapstructure:"enabled"` ChatID string `mapstructure:"chatId"` diff --git a/docs/usage.md b/docs/usage.md index ba818eb..a1bfd1d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -34,4 +34,10 @@ It is possible to configure multiple compression providers. If one fails, the ne GoBlog features a button on each post that allows you to read the post's content aloud. By default, that uses an API from the browser to generate the speech. But it's not available on all browsers and on some operating systems it sounds horrible. -There's also the possibility to configure GoBlog to use Google Cloud's Text-to-Speech API. For that take a look at the `example-config.yml` file. If configured and enabled, after publishing a post, GoBlog will automatically generate an audio file, save it to the configured media storage (local file storage by default) and safe the audio file URL to the post's `tts` parameter. After updating a post, you can manually regenerate the audio file by using the button on the post. When deleting a post or regenerating the audio, GoBlog tries to delete the old audio file as well. \ No newline at end of file +There's also the possibility to configure GoBlog to use Google Cloud's Text-to-Speech API. For that take a look at the `example-config.yml` file. If configured and enabled, after publishing a post, GoBlog will automatically generate an audio file, save it to the configured media storage (local file storage by default) and safe the audio file URL to the post's `tts` parameter. After updating a post, you can manually regenerate the audio file by using the button on the post. When deleting a post or regenerating the audio, GoBlog tries to delete the old audio file as well. + +## Notifications + +On receiving a webmention, a new comment or a contact form submission, GoBlog will create a new notification. Notifications are displayed on `/notifications` and can be deleted by the user. + +If configured, GoBlog will also send a notification using a Telegram Bot or [Ntfy.sh](https://ntfy.sh/). See the `example-config.yml` file for how to configure the notification providers. \ No newline at end of file diff --git a/example-config.yml b/example-config.yml index 095cb8f..6b61131 100644 --- a/example-config.yml +++ b/example-config.yml @@ -114,8 +114,11 @@ micropub: # Notifications notifications: + ntfy: # Receive notifications using Ntfy.sh + enabled: true # Enable it + topic: ntfy.sh/mynotificationstopic # The topic for the notifications telegram: # Receive notifications via Telegram - enabled: true + enabled: true # Enable it chatId: 123456 # Telegram chat ID (usually the user id on Telegram) botToken: BOT-TOKEN # Telegram bot token diff --git a/notifications.go b/notifications.go index 6900614..ae3eeaf 100644 --- a/notifications.go +++ b/notifications.go @@ -30,10 +30,12 @@ func (a *goBlog) sendNotification(text string) { if err := a.db.saveNotification(n); err != nil { log.Println("Failed to save notification:", err.Error()) } - if an := a.cfg.Notifications; an != nil { - _, _, err := a.send(an.Telegram, n.Text, "") - if err != nil { - log.Println("Failed to send Telegram notification:", err.Error()) + if cfg := a.cfg.Notifications; cfg != nil { + if err := a.sendNtfy(cfg.Ntfy, n.Text); err != nil { + log.Println("Failed to send notification to Ntfy:", err.Error()) + } + if _, _, err := a.sendTelegram(cfg.Telegram, n.Text, ""); err != nil { + log.Println("Failed to send notification to Telegram:", err.Error()) } } } diff --git a/ntfy.go b/ntfy.go new file mode 100644 index 0000000..016ea2d --- /dev/null +++ b/ntfy.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "strings" + + "github.com/carlmjohnson/requests" +) + +func (ntfy *configNtfy) enabled() bool { + if ntfy == nil || !ntfy.Enabled || ntfy.Topic == "" { + return false + } + return true +} + +func (a *goBlog) sendNtfy(cfg *configNtfy, msg string) error { + if !cfg.enabled() { + return nil + } + return requests. + URL(cfg.Topic). + Client(a.httpClient). + UserAgent(appUserAgent). + Post(). + BodyReader(strings.NewReader(msg)). + Fetch(context.Background()) +} diff --git a/ntfy_test.go b/ntfy_test.go new file mode 100644 index 0000000..897fb9d --- /dev/null +++ b/ntfy_test.go @@ -0,0 +1,66 @@ +package main + +import ( + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_ntfySending(t *testing.T) { + fakeClient := newFakeHttpClient() + fakeClient.setHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {})) + + app := &goBlog{ + cfg: createDefaultTestConfig(t), + httpClient: fakeClient.Client, + } + app.cfg.Notifications = &configNotifications{ + Ntfy: &configNtfy{ + Enabled: true, + Topic: "example.com/topic", + }, + } + + _ = app.initConfig() + _ = app.initDatabase(false) + app.initComponents(true) + + app.sendNotification("Test notification") + + req := fakeClient.req + + require.NotNil(t, req) + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, "https://example.com/topic", req.URL.String()) + + reqBody, _ := req.GetBody() + reqBodyByte, _ := io.ReadAll(reqBody) + + assert.Equal(t, "Test notification", string(reqBodyByte)) + + res := fakeClient.res + + require.NotNil(t, res) + assert.Equal(t, http.StatusOK, res.StatusCode) +} + +func Test_ntfyConfig(t *testing.T) { + var cfg *configNtfy + + assert.False(t, cfg.enabled()) + + cfg = &configNtfy{} + + assert.False(t, cfg.enabled()) + + cfg.Enabled = true + + assert.False(t, cfg.enabled()) + + cfg.Topic = "example.com/topic" + + assert.True(t, cfg.enabled()) +} diff --git a/telegram.go b/telegram.go index af59af2..da7cc9a 100644 --- a/telegram.go +++ b/telegram.go @@ -25,7 +25,7 @@ func (a *goBlog) initTelegram() { return } // Send message - chatId, msgId, err := a.send(tg, html, tgbotapi.ModeHTML) + chatId, msgId, err := a.sendTelegram(tg, html, tgbotapi.ModeHTML) if err != nil { log.Printf("Failed to send post to Telegram: %v", err) return @@ -71,7 +71,7 @@ func (a *goBlog) initTelegram() { return } // Send update - err = a.update(tg, chatId, messageId, html, "HTML") + err = a.updateTelegram(tg, chatId, messageId, html, "HTML") if err != nil { log.Printf("Failed to send update to Telegram: %v", err) } @@ -98,7 +98,7 @@ func (a *goBlog) initTelegram() { return } // Delete message - err = a.delete(tg, chatId, messageId) + err = a.deleteTelegram(tg, chatId, messageId) if err != nil { log.Printf("Failed to delete Telegram message: %v", err) } @@ -134,7 +134,7 @@ func (tg *configTelegram) generateHTML(title, fullURL, shortURL string) string { return message.String() } -func (a *goBlog) send(tg *configTelegram, message, mode string) (int64, int, error) { +func (a *goBlog) sendTelegram(tg *configTelegram, message, mode string) (int64, int, error) { if !tg.enabled() { return 0, 0, nil } @@ -156,7 +156,7 @@ func (a *goBlog) send(tg *configTelegram, message, mode string) (int64, int, err return res.Chat.ID, res.MessageID, nil } -func (a *goBlog) update(tg *configTelegram, chatId int64, messageId int, message, mode string) error { +func (a *goBlog) updateTelegram(tg *configTelegram, chatId int64, messageId int, message, mode string) error { if !tg.enabled() { return nil } @@ -189,7 +189,7 @@ func (a *goBlog) update(tg *configTelegram, chatId int64, messageId int, message return err } -func (a *goBlog) delete(tg *configTelegram, chatId int64, messageId int) error { +func (a *goBlog) deleteTelegram(tg *configTelegram, chatId int64, messageId int) error { if !tg.enabled() { return nil } diff --git a/telegram_test.go b/telegram_test.go index d0a8514..5222af3 100644 --- a/telegram_test.go +++ b/telegram_test.go @@ -67,7 +67,7 @@ func Test_configTelegram_send(t *testing.T) { httpClient: fakeClient.Client, } - chatId, msgId, err := app.send(tg, "Message", "HTML") + chatId, msgId, err := app.sendTelegram(tg, "Message", "HTML") require.Nil(t, err) assert.Equal(t, 123, msgId)