Skip to content

Commit

Permalink
feat(integration-discord): add discord integration
Browse files Browse the repository at this point in the history
  • Loading branch information
captainark committed Jan 5, 2025
1 parent 79ec6ef commit 64497f4
Show file tree
Hide file tree
Showing 26 changed files with 199 additions and 4 deletions.
8 changes: 8 additions & 0 deletions internal/database/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -969,4 +969,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 discord_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN discord_webhook_link text default '';
`
_, err = tx.Exec(sql)
return err
},
}
99 changes: 99 additions & 0 deletions internal/integration/discord/discord.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// Discord Webhooks documentation: https://discord.com/developers/docs/resources/webhook

package discord // import "miniflux.app/v2/internal/integration/discord"

import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"time"

"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/version"
)

const defaultClientTimeout = 10 * time.Second
const discordMsgColor = 5793266

type Client struct {
webhookURL string
}

func NewClient(webhookURL string) *Client {
return &Client{webhookURL: webhookURL}
}

func (c *Client) SendDiscordMsg(feed *model.Feed, entries model.Entries) error {

for _, entry := range entries {
requestBody, err := json.Marshal(&discordMessage{
Embeds: []discordEmbed{
{
Title: entry.Title,
Url: entry.URL,
Description: feed.Title,
Color: discordMsgColor,
Footer: &discordFooter{
Text: entry.Author + " " + "•" + " " + "Miniflux/" +version.Version,
IconUrl: feed.IconURL,
},
},
},
})
if err != nil {
return fmt.Errorf("discord: unable to encode request body: %v", err)
}

request, err := http.NewRequest(http.MethodPost, c.webhookURL, bytes.NewReader(requestBody))
if err != nil {
return fmt.Errorf("discord: unable to create request: %v", err)
}

request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Miniflux/"+version.Version)

slog.Debug("Sending Discord 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("discord: unable to send request: %v", err)
}
defer response.Body.Close()

if response.StatusCode >= 400 {
return fmt.Errorf("discord: unable to send a notification: url=%s status=%d", c.webhookURL, response.StatusCode)
}
}

return nil
}

type discordFooter struct {
Text string `json:"text,omitempty"`
IconUrl string `json:"icon_url,omitempty"`
}

type discordEmbed struct {
Title string `json:"title,omitempty"`
Url string `json:"url,omitempty"`
Description string `json:"description,omitempty"`
Color int `json:"color,omitempty"`
Footer *discordFooter `json:"footer,omitempty"`
}

type discordMessage struct {
Username string `json:"username,omitempty"`
AvatarUrl string `json:"avatar_url,omitempty"`
Content string `json:"content,omitempty"`
Embeds []discordEmbed `json:"embeds,omitempty"`
}
17 changes: 17 additions & 0 deletions internal/integration/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"miniflux.app/v2/internal/integration/apprise"
"miniflux.app/v2/internal/integration/betula"
"miniflux.app/v2/internal/integration/cubox"
"miniflux.app/v2/internal/integration/discord"
"miniflux.app/v2/internal/integration/espial"
"miniflux.app/v2/internal/integration/instapaper"
"miniflux.app/v2/internal/integration/linkace"
Expand Down Expand Up @@ -535,6 +536,22 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
}
}

if userIntegrations.DiscordEnabled {
slog.Debug("Sending new entries to Discord",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int("nb_entries", len(entries)),
slog.Int64("feed_id", feed.ID),
)

client := discord.NewClient(
userIntegrations.DiscordWebhookLink,
)

if err := client.SendDiscordMsg(feed, entries); err != nil {
slog.Warn("Unable to send new entries to Discord", slog.Any("error", err))
}
}

// Integrations that only support sending individual entries
if userIntegrations.TelegramBotEnabled {
for _, entry := range entries {
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@
"form.integration.ntfy_username": "Ntfy-Benutzername (optional)",
"form.integration.ntfy_password": "Ntfy-Passwort (optional)",
"form.integration.ntfy_icon_url": "Ntfy-Symbol-URL (optional)",
"form.integration.discord_activate": "Einträge zu Discord pushen",
"form.integration.doscord_webhook_link": "Discord-Webhook-URL",
"form.api_key.label.description": "API-Schlüsselbezeichnung",
"form.submit.loading": "Lade...",
"form.submit.saving": "Speichern...",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/el_EL.json
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.discord_activate": "Push entries to Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.api_key.label.description": "Ετικέτα κλειδιού API",
"form.submit.loading": "Φόρτωση...",
"form.submit.saving": "Αποθήκευση...",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.cubox_activate": "Save entries to Cubox",
"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.api_key.label.description": "API Key Label",
"form.submit.loading": "Loading…",
"form.submit.saving": "Saving…",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/es_ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@
"form.integration.ntfy_username": "Nombre de usuario de Ntfy (opcional)",
"form.integration.ntfy_password": "Contraseña de Ntfy (opcional)",
"form.integration.ntfy_icon_url": "URL del icono de Ntfy (opcional)",
"form.integration.discord_activate": "Enviar artículos a Discord",
"form.integration.discord_webhook_link": "URL de la Webhook de Discord",
"form.api_key.label.description": "Etiqueta de clave API",
"form.submit.loading": "Cargando...",
"form.submit.saving": "Guardando...",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/fi_FI.json
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.discord_activate": "Push entries to Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.api_key.label.description": "API Key Label",
"form.submit.loading": "Ladataan...",
"form.submit.saving": "Tallennetaan...",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@
"form.integration.ntfy_username": "Nom d'utilisateur Ntfy (optionnel)",
"form.integration.ntfy_password": "Mot de passe Ntfy (facultatif)",
"form.integration.ntfy_icon_url": "URL de l'icône Ntfy (facultatif)",
"form.integration.discord_activate": "Envoyer les articles vers Discord",
"form.integration.discord_webhook_link": "URL du Webhook Discord",
"form.api_key.label.description": "Libellé de la clé d'API",
"form.submit.loading": "Chargement...",
"form.submit.saving": "Sauvegarde en cours...",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/hi_IN.json
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.discord_activate": "Push entries to Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.api_key.label.description": "एपीआई कुंजी लेबल",
"form.submit.loading": "लोड हो रहा है...",
"form.submit.saving": "सहेजा जा रहा है...",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/id_ID.json
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.discord_activate": "Push entries to Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.api_key.label.description": "Label Kunci API",
"form.submit.loading": "Memuat...",
"form.submit.saving": "Menyimpan...",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/it_IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,8 @@
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.discord_activate": "Push entries to Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.submit.loading": "Caricamento in corso...",
"form.submit.saving": "Salvataggio in corso...",
"time_elapsed.not_yet": "non ancora",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/ja_JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.discord_activate": "Push entries to Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.api_key.label.description": "API キーラベル",
"form.submit.loading": "読み込み中…",
"form.submit.saving": "保存中…",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/nl_NL.json
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@
"form.integration.ntfy_username": "Ntfy gebruikersnaam (optioneel)",
"form.integration.ntfy_password": "Ntfy wachtwoord (optioneel)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optioneel)",
"form.integration.discord_activate": "Artikelen opslaan in Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.api_key.label.description": "API-sleutel omschrijving",
"form.submit.loading": "Laden...",
"form.submit.saving": "Opslaan...",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/pl_PL.json
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@
"form.integration.ntfy_username": "Login do ntfy (opcjonalny)",
"form.integration.ntfy_password": "Hasło do ntfy (opcjonalne)",
"form.integration.ntfy_icon_url": "Adres URL ikony ntfy (opcjonalny)",
"form.integration.discord_activate": "Przesyłaj wpisy do Discord",
"form.integration.discord_webhook_link": "Adres URL Webhook Discord",
"form.api_key.label.description": "Etykieta klucza API",
"form.submit.loading": "Ładowanie…",
"form.submit.saving": "Zapisywanie…",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/pt_BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.discord_activate": "Push entries to Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.api_key.label.description": "Etiqueta da chave de API",
"form.submit.loading": "Carregando...",
"form.submit.saving": "Salvando...",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/ru_RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.discord_activate": "Отправить статьи в Discord",
"form.integration.discord_webhook_link": "Ссылка на Discord Webhook",
"form.api_key.label.description": "Описание API-ключа",
"form.submit.loading": "Загрузка…",
"form.submit.saving": "Сохранение…",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/tr_TR.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.integration.cubox_activate": "Save entries to Cubox",
"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.prefs.fieldset.application_settings": "Uygulama Ayarları",
"form.prefs.fieldset.authentication_settings": "Kimlik Doğrulama Ayarları",
"form.prefs.fieldset.reader_settings": "Okuyucu Ayarları",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/uk_UA.json
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.discord_activate": "Push entries to Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.api_key.label.description": "Назва ключа API",
"form.submit.loading": "Завантаження...",
"form.submit.saving": "Зберігаю...",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@
"form.integration.ntfy_username": "Ntfy用户名(可选)",
"form.integration.ntfy_password": "Ntfy密码(可选)",
"form.integration.ntfy_icon_url": "Ntfy图标URL(可选)",
"form.integration.discord_activate": "将新文章推送到 Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.api_key.label.description": "API密钥标签",
"form.submit.loading": "载入中…",
"form.submit.saving": "保存中…",
Expand Down
2 changes: 2 additions & 0 deletions internal/locale/translations/zh_TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.discord_activate": "推送文章到 Discord",
"form.integration.discord_webhook_link": "Discord Webhook link",
"form.api_key.label.description": "API金鑰標籤",
"form.submit.loading": "載入中…",
"form.submit.saving": "儲存中…",
Expand Down
2 changes: 2 additions & 0 deletions internal/model/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,6 @@ type Integration struct {
NtfyIconURL string
CuboxEnabled bool
CuboxAPILink string
DiscordEnabled bool
DiscordWebhookLink string
}
17 changes: 13 additions & 4 deletions internal/storage/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
ntfy_password,
ntfy_icon_url,
cubox_enabled,
cubox_api_link
cubox_api_link,
discord_enabled,
discord_webhook_link
FROM
integrations
WHERE
Expand Down Expand Up @@ -318,6 +320,8 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
&integration.NtfyIconURL,
&integration.CuboxEnabled,
&integration.CuboxAPILink,
&integration.DiscordEnabled,
&integration.DiscordWebhookLink,
)
switch {
case err == sql.ErrNoRows:
Expand Down Expand Up @@ -434,9 +438,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
ntfy_password=$97,
ntfy_icon_url=$98,
cubox_enabled=$99,
cubox_api_link=$100
cubox_api_link=$100,
discord_enabled=$101,
discord_webhook_link=$102
WHERE
user_id=$101
user_id=$103
`
_, err := s.db.Exec(
query,
Expand Down Expand Up @@ -540,6 +546,8 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
integration.NtfyIconURL,
integration.CuboxEnabled,
integration.CuboxAPILink,
integration.DiscordEnabled,
integration.DiscordWebhookLink,
integration.UserID,
)

Expand Down Expand Up @@ -580,7 +588,8 @@ func (s *Storage) HasSaveEntry(userID int64) (result bool) {
omnivore_enabled='t' OR
raindrop_enabled='t' OR
betula_enabled='t' OR
cubox_enabled='t'
cubox_enabled='t' OR
discord_enabled='t'
)
`
if err := s.db.QueryRow(query, userID).Scan(&result); err != nil {
Expand Down
16 changes: 16 additions & 0 deletions internal/template/templates/views/integrations.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ <h1 id="page-header-title">{{ t "page.integrations.title" }}</h1>
</div>
</details>

<details {{ if .form.DiscordEnabled }}open{{ end }}>
<summary>Discord</summary>
<div class="form-section">
<label>
<input type="checkbox" name="discord_enabled" value="1" {{ if .form.DiscordEnabled }}checked{{ end }}> {{ t "form.integration.discord_activate" }}
</label>

<label for="form-discord-api-link">{{ t "form.integration.discord_webhook_link" }}</label>
<input type="url" name="discord_webhook_link" id="form-discord-webhook-link" value="{{ .form.DiscordWebhookLink }}" placeholder="https://discord.com/api/webhooks/xxx/xxx" 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.EspialEnabled }}open{{ end }}>
<summary>Espial</summary>
<div class="form-section">
Expand Down
Loading

0 comments on commit 64497f4

Please sign in to comment.