From c058f86bff02ab7397389b6682c20088c4217df1 Mon Sep 17 00:00:00 2001 From: CaptainArk Date: Thu, 2 Jan 2025 23:37:43 +0100 Subject: [PATCH] feat(integration-discord): add discord integration --- internal/database/migrations.go | 8 ++ internal/integration/discord/discord.go | 98 +++++++++++++++++++ internal/integration/integration.go | 17 ++++ internal/locale/translations/de_DE.json | 2 + internal/locale/translations/el_EL.json | 2 + internal/locale/translations/en_US.json | 2 + internal/locale/translations/es_ES.json | 2 + internal/locale/translations/fi_FI.json | 2 + internal/locale/translations/fr_FR.json | 2 + internal/locale/translations/hi_IN.json | 2 + internal/locale/translations/id_ID.json | 2 + internal/locale/translations/it_IT.json | 2 + internal/locale/translations/ja_JP.json | 2 + internal/locale/translations/nl_NL.json | 2 + internal/locale/translations/pl_PL.json | 2 + internal/locale/translations/pt_BR.json | 2 + internal/locale/translations/ru_RU.json | 2 + internal/locale/translations/tr_TR.json | 2 + internal/locale/translations/uk_UA.json | 2 + internal/locale/translations/zh_CN.json | 2 + internal/locale/translations/zh_TW.json | 2 + internal/model/integration.go | 2 + internal/storage/integration.go | 17 +++- .../templates/views/integrations.html | 16 +++ internal/ui/form/integration.go | 6 ++ internal/ui/integration_show.go | 2 + 26 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 internal/integration/discord/discord.go diff --git a/internal/database/migrations.go b/internal/database/migrations.go index c8a950e079d..0e8e7372e62 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -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 + }, } diff --git a/internal/integration/discord/discord.go b/internal/integration/discord/discord.go new file mode 100644 index 00000000000..21fe8551175 --- /dev/null +++ b/internal/integration/discord/discord.go @@ -0,0 +1,98 @@ +// 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"` +} diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 5910baaab7c..e363af9ee90 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -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" @@ -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 { diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json index c983c74f268..2bce3778544 100644 --- a/internal/locale/translations/de_DE.json +++ b/internal/locale/translations/de_DE.json @@ -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.discord_webhook_link": "Discord-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 59158fd1fad..4e522e77d39 100644 --- a/internal/locale/translations/el_EL.json +++ b/internal/locale/translations/el_EL.json @@ -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": "Αποθήκευση...", diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index 4ca46332d86..4faa30ebb10 100644 --- a/internal/locale/translations/en_US.json +++ b/internal/locale/translations/en_US.json @@ -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…", diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json index f3d02bef77b..39cfad2d7cd 100644 --- a/internal/locale/translations/es_ES.json +++ b/internal/locale/translations/es_ES.json @@ -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...", diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json index 0022f84f064..157e4240f14 100644 --- a/internal/locale/translations/fi_FI.json +++ b/internal/locale/translations/fi_FI.json @@ -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...", diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json index 54c02aaac01..c3f50d607cf 100644 --- a/internal/locale/translations/fr_FR.json +++ b/internal/locale/translations/fr_FR.json @@ -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...", diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json index 39eeb4bb908..f4cadcd6e52 100644 --- a/internal/locale/translations/hi_IN.json +++ b/internal/locale/translations/hi_IN.json @@ -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": "सहेजा जा रहा है...", diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json index 8250adb3132..5434ec94831 100644 --- a/internal/locale/translations/id_ID.json +++ b/internal/locale/translations/id_ID.json @@ -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...", diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json index f5ac1817e8e..757449e1e0f 100644 --- a/internal/locale/translations/it_IT.json +++ b/internal/locale/translations/it_IT.json @@ -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", diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json index b835cd07ec3..8053cbf649d 100644 --- a/internal/locale/translations/ja_JP.json +++ b/internal/locale/translations/ja_JP.json @@ -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": "保存中…", diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json index 0ce2b6dac8a..a97ad227418 100644 --- a/internal/locale/translations/nl_NL.json +++ b/internal/locale/translations/nl_NL.json @@ -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...", diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json index bbc4d0039ac..b6aaae87d84 100644 --- a/internal/locale/translations/pl_PL.json +++ b/internal/locale/translations/pl_PL.json @@ -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…", diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json index 31e2e02218a..a7a3596b261 100644 --- a/internal/locale/translations/pt_BR.json +++ b/internal/locale/translations/pt_BR.json @@ -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...", diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json index 6344ade6819..e9256862c18 100644 --- a/internal/locale/translations/ru_RU.json +++ b/internal/locale/translations/ru_RU.json @@ -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": "Сохранение…", diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json index 8e7efa8ad25..eac7ee9ee41 100644 --- a/internal/locale/translations/tr_TR.json +++ b/internal/locale/translations/tr_TR.json @@ -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ı", diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json index 1c3f932e739..a82aa7d1fd5 100644 --- a/internal/locale/translations/uk_UA.json +++ b/internal/locale/translations/uk_UA.json @@ -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": "Зберігаю...", diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json index 59bfc4d6b8d..ccf3daab822 100644 --- a/internal/locale/translations/zh_CN.json +++ b/internal/locale/translations/zh_CN.json @@ -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": "保存中…", diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json index 4026ac6a369..fb48f80b8a0 100644 --- a/internal/locale/translations/zh_TW.json +++ b/internal/locale/translations/zh_TW.json @@ -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": "儲存中…", diff --git a/internal/model/integration.go b/internal/model/integration.go index 5b8d70abcba..d8734b6e266 100644 --- a/internal/model/integration.go +++ b/internal/model/integration.go @@ -106,4 +106,6 @@ type Integration struct { NtfyIconURL string CuboxEnabled bool CuboxAPILink string + DiscordEnabled bool + DiscordWebhookLink string } diff --git a/internal/storage/integration.go b/internal/storage/integration.go index dea3369d1bd..8502b1caf98 100644 --- a/internal/storage/integration.go +++ b/internal/storage/integration.go @@ -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 @@ -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: @@ -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, @@ -540,6 +546,8 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error { integration.NtfyIconURL, integration.CuboxEnabled, integration.CuboxAPILink, + integration.DiscordEnabled, + integration.DiscordWebhookLink, integration.UserID, ) @@ -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 { diff --git a/internal/template/templates/views/integrations.html b/internal/template/templates/views/integrations.html index 469abcd5e87..9878c5dfafc 100644 --- a/internal/template/templates/views/integrations.html +++ b/internal/template/templates/views/integrations.html @@ -73,6 +73,22 @@

{{ t "page.integrations.title" }}

+
+ Discord +
+ + + + + +
+ +
+
+
+
Espial
diff --git a/internal/ui/form/integration.go b/internal/ui/form/integration.go index 0852123be6a..3049e520814 100644 --- a/internal/ui/form/integration.go +++ b/internal/ui/form/integration.go @@ -112,6 +112,8 @@ type IntegrationForm struct { NtfyIconURL string CuboxEnabled bool CuboxAPILink string + DiscordEnabled bool + DiscordWebhookLink string } // Merge copy form values to the model. @@ -213,6 +215,8 @@ func (i IntegrationForm) Merge(integration *model.Integration) { integration.NtfyIconURL = i.NtfyIconURL integration.CuboxEnabled = i.CuboxEnabled integration.CuboxAPILink = i.CuboxAPILink + integration.DiscordEnabled = i.DiscordEnabled + integration.DiscordWebhookLink = i.DiscordWebhookLink } // NewIntegrationForm returns a new IntegrationForm. @@ -317,6 +321,8 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm { NtfyIconURL: r.FormValue("ntfy_icon_url"), CuboxEnabled: r.FormValue("cubox_enabled") == "1", CuboxAPILink: r.FormValue("cubox_api_link"), + DiscordEnabled: r.FormValue("discord_enabled") == "1", + DiscordWebhookLink: r.FormValue("discord_webhook_link"), } } diff --git a/internal/ui/integration_show.go b/internal/ui/integration_show.go index 1395fab263c..a6e0ece37d5 100644 --- a/internal/ui/integration_show.go +++ b/internal/ui/integration_show.go @@ -126,6 +126,8 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) { NtfyIconURL: integration.NtfyIconURL, CuboxEnabled: integration.CuboxEnabled, CuboxAPILink: integration.CuboxAPILink, + DiscordEnabled: integration.DiscordEnabled, + DiscordWebhookLink: integration.DiscordWebhookLink, } sess := session.New(h.store, request.SessionID(r))