mirror of
https://github.com/miniflux/v2.git
synced 2025-06-27 16:36:00 +00:00
feat(integration): add Slack integration
This commit is contained in:
parent
bae872e79b
commit
fba23cf464
26 changed files with 213 additions and 4 deletions
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
113
internal/integration/slack/slack.go
Normal file
113
internal/integration/slack/slack.go
Normal file
|
@ -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"`
|
||||
}
|
|
@ -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...",
|
||||
|
|
|
@ -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": "Αποθήκευση...",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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": "सहेजा जा रहा है...",
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "保存中…",
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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": "Сохранение…",
|
||||
|
|
|
@ -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ı",
|
||||
|
|
|
@ -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": "Зберігаю...",
|
||||
|
|
|
@ -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": "保存中…",
|
||||
|
|
|
@ -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": "儲存中…",
|
||||
|
|
|
@ -109,4 +109,6 @@ type Integration struct {
|
|||
CuboxAPILink string
|
||||
DiscordEnabled bool
|
||||
DiscordWebhookLink string
|
||||
SlackEnabled bool
|
||||
SlackWebhookLink string
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -535,6 +535,22 @@
|
|||
</div>
|
||||
</details>
|
||||
|
||||
<details {{ if .form.SlackEnabled }}open{{ end }}>
|
||||
<summary>Slack</summary>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="slack_enabled" value="1" {{ if .form.SlackEnabled }}checked{{ end }}> {{ t "form.integration.slack_activate" }}
|
||||
</label>
|
||||
|
||||
<label for="form-slack-webhook-link">{{ t "form.integration.slack_webhook_link" }}</label>
|
||||
<input type="url" name="slack_webhook_link" id="form-slack-webhook-link" value="{{ .form.SlackWebhookLink }}" placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" spellcheck="false">
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details {{ if .form.TelegramBotEnabled }}open{{ end }}>
|
||||
<summary>Telegram Bot</summary>
|
||||
<div class="form-section">
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue