diff --git a/internal/database/migrations.go b/internal/database/migrations.go index 6791d817..811ef4b8 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -982,4 +982,12 @@ var migrations = []func(tx *sql.Tx, driver string) error{ _, err = tx.Exec(sql) return err }, + func(tx *sql.Tx, _ string) (err error) { + sql := ` + ALTER TABLE integrations ADD COLUMN slack_enabled bool default 'f'; + ALTER TABLE integrations ADD COLUMN slack_webhook_link text default ''; + ` + _, err = tx.Exec(sql) + return err + }, } diff --git a/internal/integration/integration.go b/internal/integration/integration.go index ebca70af..029efbbd 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -28,6 +28,7 @@ import ( "miniflux.app/v2/internal/integration/readwise" "miniflux.app/v2/internal/integration/shaarli" "miniflux.app/v2/internal/integration/shiori" + "miniflux.app/v2/internal/integration/slack" "miniflux.app/v2/internal/integration/telegrambot" "miniflux.app/v2/internal/integration/wallabag" "miniflux.app/v2/internal/integration/webhook" @@ -553,6 +554,22 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode } } + if userIntegrations.SlackEnabled { + slog.Debug("Sending new entries to Slack", + slog.Int64("user_id", userIntegrations.UserID), + slog.Int("nb_entries", len(entries)), + slog.Int64("feed_id", feed.ID), + ) + + client := slack.NewClient( + userIntegrations.SlackWebhookLink, + ) + + if err := client.SendSlackMsg(feed, entries); err != nil { + slog.Warn("Unable to send new entries to Slack", slog.Any("error", err)) + } + } + // Integrations that only support sending individual entries if userIntegrations.TelegramBotEnabled { for _, entry := range entries { diff --git a/internal/integration/slack/slack.go b/internal/integration/slack/slack.go new file mode 100644 index 00000000..214b9657 --- /dev/null +++ b/internal/integration/slack/slack.go @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Slack Webhooks documentation: https://api.slack.com/messaging/webhooks + +package slack // import "miniflux.app/v2/internal/integration/slack" + +import ( + "bytes" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "time" + + "miniflux.app/v2/internal/model" + "miniflux.app/v2/internal/urllib" + "miniflux.app/v2/internal/version" +) + +const defaultClientTimeout = 10 * time.Second +const slackMsgColor = "#5865F2" + +type Client struct { + webhookURL string +} + +func NewClient(webhookURL string) *Client { + return &Client{webhookURL: webhookURL} +} + +func (c *Client) SendSlackMsg(feed *model.Feed, entries model.Entries) error { + for _, entry := range entries { + requestBody, err := json.Marshal(&slackMessage{ + Attachments: []slackAttachments{ + { + Title: "RSS feed update from Miniflux", + Color: slackMsgColor, + Fields: []slackFields{ + { + Title: "Updated feed", + Value: feed.Title, + }, + { + Title: "Article title", + Value: entry.Title, + }, + { + Title: "Article link", + Value: entry.URL, + }, + { + Title: "Author", + Value: entry.Author, + Short: true, + }, + { + Title: "Source website", + Value: urllib.RootURL(feed.SiteURL), + Short: true, + }, + }, + }, + }, + }) + if err != nil { + return fmt.Errorf("slack: unable to encode request body: %v", err) + } + + request, err := http.NewRequest(http.MethodPost, c.webhookURL, bytes.NewReader(requestBody)) + if err != nil { + return fmt.Errorf("slack: unable to create request: %v", err) + } + + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "Miniflux/"+version.Version) + + slog.Debug("Sending Slack notification", + slog.String("webhookURL", c.webhookURL), + slog.String("title", feed.Title), + slog.String("entry_url", entry.URL), + ) + + httpClient := &http.Client{Timeout: defaultClientTimeout} + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("slack: unable to send request: %v", err) + } + response.Body.Close() + + if response.StatusCode >= 400 { + return fmt.Errorf("slack: unable to send a notification: url=%s status=%d", c.webhookURL, response.StatusCode) + } + } + + return nil +} + +type slackFields struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short,omitempty"` +} + +type slackAttachments struct { + Title string `json:"title"` + Color string `json:"color"` + Fields []slackFields `json:"fields"` +} + +type slackMessage struct { + Attachments []slackAttachments `json:"attachments"` +} diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json index 1bb30808..34de24e8 100644 --- a/internal/locale/translations/de_DE.json +++ b/internal/locale/translations/de_DE.json @@ -516,6 +516,8 @@ "form.integration.ntfy_internal_links": "Interne Links beim Klicken verwenden (optional)", "form.integration.discord_activate": "Einträge zu Discord pushen", "form.integration.discord_webhook_link": "Discord-Webhook-URL", + "form.integration.slack_activate": "Einträge zu Slack pushen", + "form.integration.slack_webhook_link": "Slack-Webhook-URL", "form.api_key.label.description": "API-Schlüsselbezeichnung", "form.submit.loading": "Lade...", "form.submit.saving": "Speichern...", diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json index 6ce997a3..550711af 100644 --- a/internal/locale/translations/el_EL.json +++ b/internal/locale/translations/el_EL.json @@ -516,6 +516,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Push entries to Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "Push entries to Slack", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.api_key.label.description": "Ετικέτα κλειδιού API", "form.submit.loading": "Φόρτωση...", "form.submit.saving": "Αποθήκευση...", diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index 27b0692d..287770a4 100644 --- a/internal/locale/translations/en_US.json +++ b/internal/locale/translations/en_US.json @@ -516,6 +516,8 @@ "form.integration.cubox_api_link": "Cubox API link", "form.integration.discord_activate": "Push entries to Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "Push entries to Slack", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.api_key.label.description": "API Key Label", "form.submit.loading": "Loading…", "form.submit.saving": "Saving…", diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json index 9c1633ba..ac923c06 100644 --- a/internal/locale/translations/es_ES.json +++ b/internal/locale/translations/es_ES.json @@ -516,6 +516,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Enviar artículos a Discord", "form.integration.discord_webhook_link": "URL de la Webhook de Discord", + "form.integration.slack_activate": "Enviar artículos a Slack", + "form.integration.slack_webhook_link": "URL de la Webhook de Slack", "form.api_key.label.description": "Etiqueta de clave API", "form.submit.loading": "Cargando...", "form.submit.saving": "Guardando...", diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json index f9c6923f..a4de3c8f 100644 --- a/internal/locale/translations/fi_FI.json +++ b/internal/locale/translations/fi_FI.json @@ -516,6 +516,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Push entries to Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "Push entries to Slack", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.api_key.label.description": "API Key Label", "form.submit.loading": "Ladataan...", "form.submit.saving": "Tallennetaan...", diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json index 20ca92ba..ce917d88 100644 --- a/internal/locale/translations/fr_FR.json +++ b/internal/locale/translations/fr_FR.json @@ -516,6 +516,8 @@ "form.integration.ntfy_internal_links": "Utiliser les liens internes vers Miniflux (facultatif)", "form.integration.discord_activate": "Envoyer les articles vers Discord", "form.integration.discord_webhook_link": "URL du Webhook Discord", + "form.integration.slack_activate": "Envoyer les articles vers Slack", + "form.integration.slack_webhook_link": "URL du Webhook Slack", "form.api_key.label.description": "Libellé de la clé d'API", "form.submit.loading": "Chargement...", "form.submit.saving": "Sauvegarde en cours...", diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json index c40f83df..af404f67 100644 --- a/internal/locale/translations/hi_IN.json +++ b/internal/locale/translations/hi_IN.json @@ -516,6 +516,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Push entries to Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "Push entries to Slack", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.api_key.label.description": "एपीआई कुंजी लेबल", "form.submit.loading": "लोड हो रहा है...", "form.submit.saving": "सहेजा जा रहा है...", diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json index 40a9f13f..4cc1fb1a 100644 --- a/internal/locale/translations/id_ID.json +++ b/internal/locale/translations/id_ID.json @@ -506,6 +506,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Push entries to Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "Push entries to Slack", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.api_key.label.description": "Label Kunci API", "form.submit.loading": "Memuat...", "form.submit.saving": "Menyimpan...", diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json index f7e3d34b..d13ab673 100644 --- a/internal/locale/translations/it_IT.json +++ b/internal/locale/translations/it_IT.json @@ -517,6 +517,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Push entries to Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "Push entries to Slack", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.submit.loading": "Caricamento in corso...", "form.submit.saving": "Salvataggio in corso...", "time_elapsed.not_yet": "non ancora", diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json index 2a2568b3..64a4d514 100644 --- a/internal/locale/translations/ja_JP.json +++ b/internal/locale/translations/ja_JP.json @@ -506,6 +506,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Push entries to Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "Slack entries to Discord", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.api_key.label.description": "API キーラベル", "form.submit.loading": "読み込み中…", "form.submit.saving": "保存中…", diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json index 4f02bb39..f7d9a066 100644 --- a/internal/locale/translations/nl_NL.json +++ b/internal/locale/translations/nl_NL.json @@ -516,6 +516,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Artikelen opslaan in Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "Artikelen opslaan in Slack", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.api_key.label.description": "API-sleutel omschrijving", "form.submit.loading": "Laden...", "form.submit.saving": "Opslaan...", diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json index 198478d6..d95912d4 100644 --- a/internal/locale/translations/pl_PL.json +++ b/internal/locale/translations/pl_PL.json @@ -526,6 +526,8 @@ "form.integration.ntfy_internal_links": "Używaj łączy wewnętrznych po kliknięciu (opcjonalnie)", "form.integration.discord_activate": "Przesyłaj wpisy do Discord", "form.integration.discord_webhook_link": "Adres URL Webhook Discord", + "form.integration.slack_activate": "Przesyłaj wpisy do Slack", + "form.integration.slack_webhook_link": "Adres URL Webhook Slack", "form.api_key.label.description": "Etykieta klucza API", "form.submit.loading": "Ładowanie…", "form.submit.saving": "Zapisywanie…", diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json index b6aa624f..3e024f12 100644 --- a/internal/locale/translations/pt_BR.json +++ b/internal/locale/translations/pt_BR.json @@ -516,6 +516,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Push entries to Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "Slack entries to Discord", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.api_key.label.description": "Etiqueta da chave de API", "form.submit.loading": "Carregando...", "form.submit.saving": "Salvando...", diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json index 36421ba4..bd1c09c9 100644 --- a/internal/locale/translations/ru_RU.json +++ b/internal/locale/translations/ru_RU.json @@ -526,6 +526,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Отправить статьи в Discord", "form.integration.discord_webhook_link": "Ссылка на Discord Webhook", + "form.integration.slack_activate": "Отправить статьи в Slack", + "form.integration.slack_webhook_link": "Ссылка на Slack Webhook", "form.api_key.label.description": "Описание API-ключа", "form.submit.loading": "Загрузка…", "form.submit.saving": "Сохранение…", diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json index 203a7017..8f5f2860 100644 --- a/internal/locale/translations/tr_TR.json +++ b/internal/locale/translations/tr_TR.json @@ -291,6 +291,8 @@ "form.integration.cubox_api_link": "Cubox API link", "form.integration.discord_activate": "Makaleleri Discord'a gönder", "form.integration.discord_webhook_link": "Discord hizmet Webhook'lerinin virgülle ayrılmış listesi", + "form.integration.slack_activate": "Makaleleri Slack'a gönder", + "form.integration.slack_webhook_link": "Slack hizmet Webhook'lerinin virgülle ayrılmış listesi", "form.prefs.fieldset.application_settings": "Uygulama Ayarları", "form.prefs.fieldset.authentication_settings": "Kimlik Doğrulama Ayarları", "form.prefs.fieldset.reader_settings": "Okuyucu Ayarları", diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json index 684bf0d6..6008666b 100644 --- a/internal/locale/translations/uk_UA.json +++ b/internal/locale/translations/uk_UA.json @@ -526,6 +526,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "Push entries to Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "Slack entries to Discord", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.api_key.label.description": "Назва ключа API", "form.submit.loading": "Завантаження...", "form.submit.saving": "Зберігаю...", diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json index 564bc2b9..a9216919 100644 --- a/internal/locale/translations/zh_CN.json +++ b/internal/locale/translations/zh_CN.json @@ -506,6 +506,8 @@ "form.integration.ntfy_internal_links": "Use internal links on click (optional)", "form.integration.discord_activate": "将新文章推送到 Discord", "form.integration.discord_webhook_link": "Discord Webhook link", + "form.integration.slack_activate": "将新文章推送到 Slack", + "form.integration.slack_webhook_link": "Slack Webhook link", "form.api_key.label.description": "API密钥标签", "form.submit.loading": "载入中…", "form.submit.saving": "保存中…", diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json index c51c0ebe..cb13525e 100644 --- a/internal/locale/translations/zh_TW.json +++ b/internal/locale/translations/zh_TW.json @@ -506,6 +506,8 @@ "form.integration.ntfy_internal_links": "點選時使用內部連結 (選填)", "form.integration.discord_activate": "推送文章到 Discord", "form.integration.discord_webhook_link": "Discord Webhook 連結", + "form.integration.slack_activate": "推送文章到 Slack", + "form.integration.slack_webhook_link": "Slack Webhook 連結", "form.api_key.label.description": "API 金鑰標籤", "form.submit.loading": "載入中…", "form.submit.saving": "儲存中…", diff --git a/internal/model/integration.go b/internal/model/integration.go index 0a4e466a..ccb33f85 100644 --- a/internal/model/integration.go +++ b/internal/model/integration.go @@ -109,4 +109,6 @@ type Integration struct { CuboxAPILink string DiscordEnabled bool DiscordWebhookLink string + SlackEnabled bool + SlackWebhookLink string } diff --git a/internal/storage/integration.go b/internal/storage/integration.go index 32da349d..0b9ee6c4 100644 --- a/internal/storage/integration.go +++ b/internal/storage/integration.go @@ -212,7 +212,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) { cubox_enabled, cubox_api_link, discord_enabled, - discord_webhook_link + discord_webhook_link, + slack_enabled, + slack_webhook_link FROM integrations WHERE @@ -324,6 +326,8 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) { &integration.CuboxAPILink, &integration.DiscordEnabled, &integration.DiscordWebhookLink, + &integration.SlackEnabled, + &integration.SlackWebhookLink, ) switch { case err == sql.ErrNoRows: @@ -443,9 +447,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error { cubox_enabled=$100, cubox_api_link=$101, discord_enabled=$102, - discord_webhook_link=$103 + discord_webhook_link=$103, + slack_enabled=$104, + slack_webhook_link=$105 WHERE - user_id=$104 + user_id=$106 ` _, err := s.db.Exec( query, @@ -552,6 +558,8 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error { integration.CuboxAPILink, integration.DiscordEnabled, integration.DiscordWebhookLink, + integration.SlackEnabled, + integration.SlackWebhookLink, integration.UserID, ) @@ -593,7 +601,8 @@ func (s *Storage) HasSaveEntry(userID int64) (result bool) { raindrop_enabled='t' OR betula_enabled='t' OR cubox_enabled='t' OR - discord_enabled='t' + discord_enabled='t' OR + slack_enabled='t' ) ` if err := s.db.QueryRow(query, userID).Scan(&result); err != nil { diff --git a/internal/template/templates/views/integrations.html b/internal/template/templates/views/integrations.html index 3e8db0e0..2f760039 100644 --- a/internal/template/templates/views/integrations.html +++ b/internal/template/templates/views/integrations.html @@ -535,6 +535,22 @@ +
+ Slack +
+ + + + + +
+ +
+
+
+
Telegram Bot
diff --git a/internal/ui/form/integration.go b/internal/ui/form/integration.go index a3a4e0b2..25b0b3e7 100644 --- a/internal/ui/form/integration.go +++ b/internal/ui/form/integration.go @@ -115,6 +115,8 @@ type IntegrationForm struct { CuboxAPILink string DiscordEnabled bool DiscordWebhookLink string + SlackEnabled bool + SlackWebhookLink string } // Merge copy form values to the model. @@ -219,6 +221,8 @@ func (i IntegrationForm) Merge(integration *model.Integration) { integration.CuboxAPILink = i.CuboxAPILink integration.DiscordEnabled = i.DiscordEnabled integration.DiscordWebhookLink = i.DiscordWebhookLink + integration.SlackEnabled = i.SlackEnabled + integration.SlackWebhookLink = i.SlackWebhookLink } // NewIntegrationForm returns a new IntegrationForm. @@ -326,6 +330,8 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm { CuboxAPILink: r.FormValue("cubox_api_link"), DiscordEnabled: r.FormValue("discord_enabled") == "1", DiscordWebhookLink: r.FormValue("discord_webhook_link"), + SlackEnabled: r.FormValue("slack_enabled") == "1", + SlackWebhookLink: r.FormValue("slack_webhook_link"), } } diff --git a/internal/ui/integration_show.go b/internal/ui/integration_show.go index 2ad50539..6e17bcee 100644 --- a/internal/ui/integration_show.go +++ b/internal/ui/integration_show.go @@ -129,6 +129,8 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) { CuboxAPILink: integration.CuboxAPILink, DiscordEnabled: integration.DiscordEnabled, DiscordWebhookLink: integration.DiscordWebhookLink, + SlackEnabled: integration.SlackEnabled, + SlackWebhookLink: integration.SlackWebhookLink, } sess := session.New(h.store, request.SessionID(r))