Răsfoiți Sursa

Improve Telegram integration

- Remove dependency on `go-telegram-bot-api`
- Add new options: optional topic ID, disable page preview, disable notifications
- Add new button to go to article
Frédéric Guillot 2 ani în urmă
părinte
comite
cb228e73ad

+ 0 - 2
go.mod

@@ -5,7 +5,6 @@ module miniflux.app/v2
 require (
 	github.com/PuerkitoBio/goquery v1.8.1
 	github.com/coreos/go-oidc/v3 v3.6.0
-	github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
 	github.com/gorilla/mux v1.8.0
 	github.com/lib/pq v1.10.9
 	github.com/mccutchen/go-httpbin/v2 v2.11.0
@@ -31,7 +30,6 @@ require (
 	github.com/prometheus/common v0.42.0 // indirect
 	github.com/prometheus/procfs v0.10.1 // indirect
 	github.com/tdewolff/parse/v2 v2.6.7 // indirect
-	github.com/technoweenie/multipartstreamer v1.0.1 // indirect
 	golang.org/x/sys v0.11.0 // indirect
 	golang.org/x/text v0.12.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect

+ 0 - 4
go.sum

@@ -13,8 +13,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
 github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
-github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
-github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
@@ -55,8 +53,6 @@ github.com/tdewolff/parse/v2 v2.6.7 h1:WrFllrqmzAcrKHzoYgMupqgUBIfBVOb0yscFzDf8b
 github.com/tdewolff/parse/v2 v2.6.7/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
 github.com/tdewolff/test v1.0.9 h1:SswqJCmeN4B+9gEAi/5uqT0qpi1y2/2O47V/1hhGZT0=
 github.com/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
-github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
-github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA=
 github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

+ 9 - 0
internal/database/migrations.go

@@ -767,4 +767,13 @@ var migrations = []func(tx *sql.Tx) error{
 		_, err = tx.Exec(sql)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		sql := `
+			ALTER TABLE integrations ADD COLUMN telegram_bot_topic_id int;
+			ALTER TABLE integrations ADD COLUMN telegram_bot_disable_web_page_preview bool default 'f';
+			ALTER TABLE integrations ADD COLUMN telegram_bot_disable_notification bool default 'f';
+		`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 15 - 8
internal/integration/integration.go

@@ -172,7 +172,7 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
 // PushEntries pushes a list of entries to activated third-party providers during feed refreshes.
 func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *model.Integration) {
 	if userIntegrations.MatrixBotEnabled {
-		logger.Debug("[Integration] Sending %d entries for User #%d to Matrix", len(entries), userIntegrations.UserID)
+		logger.Debug("[Integration] Sending %d entries for user #%d to Matrix", len(entries), userIntegrations.UserID)
 
 		err := matrixbot.PushEntries(feed, entries, userIntegrations.MatrixBotURL, userIntegrations.MatrixBotUser, userIntegrations.MatrixBotPassword, userIntegrations.MatrixBotChatID)
 		if err != nil {
@@ -193,16 +193,23 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
 	if userIntegrations.TelegramBotEnabled || userIntegrations.AppriseEnabled {
 		for _, entry := range entries {
 			if userIntegrations.TelegramBotEnabled {
-				logger.Debug("[Integration] Sending Entry %q for User #%d to Telegram", entry.URL, userIntegrations.UserID)
-
-				err := telegrambot.PushEntry(entry, userIntegrations.TelegramBotToken, userIntegrations.TelegramBotChatID)
-				if err != nil {
-					logger.Error("[Integration] push entry to telegram bot failed: %v", err)
+				logger.Debug("[Integration] Sending entry %q for user #%d to Telegram", entry.URL, userIntegrations.UserID)
+
+				if err := telegrambot.PushEntry(
+					feed,
+					entry,
+					userIntegrations.TelegramBotToken,
+					userIntegrations.TelegramBotChatID,
+					userIntegrations.TelegramBotTopicID,
+					userIntegrations.TelegramBotDisableWebPagePreview,
+					userIntegrations.TelegramBotDisableNotification,
+				); err != nil {
+					logger.Error("[Integration] %v", err)
 				}
 			}
 
 			if userIntegrations.AppriseEnabled {
-				logger.Debug("[Integration] Sending Entry %q for User #%d to apprise", entry.URL, userIntegrations.UserID)
+				logger.Debug("[Integration] Sending entry %q for user #%d to Apprise", entry.URL, userIntegrations.UserID)
 
 				appriseServiceURLs := userIntegrations.AppriseURL
 				if feed.AppriseServiceURLs != "" {
@@ -215,7 +222,7 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
 				)
 
 				if err := client.SendNotification(entry); err != nil {
-					logger.Error("[Integration] push entry to apprise failed: %v", err)
+					logger.Error("[Integration] %v", err)
 				}
 			}
 		}

+ 174 - 0
internal/integration/telegrambot/client.go

@@ -0,0 +1,174 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package telegrambot // import "miniflux.app/v2/internal/integration/telegrambot"
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"time"
+
+	"miniflux.app/v2/internal/version"
+)
+
+const (
+	defaultClientTimeout = 10 * time.Second
+	telegramAPIEndpoint  = "https://api.telegram.org"
+
+	MarkdownFormatting   = "Markdown"
+	MarkdownV2Formatting = "MarkdownV2"
+	HTMLFormatting       = "HTML"
+)
+
+type Client struct {
+	botToken string
+	chatID   string
+}
+
+func NewClient(botToken, chatID string) *Client {
+	return &Client{
+		botToken: botToken,
+		chatID:   chatID,
+	}
+}
+
+// Specs: https://core.telegram.org/bots/api#getme
+func (c *Client) GetMe() (*User, error) {
+	endpointURL, err := url.JoinPath(telegramAPIEndpoint, "/bot"+c.botToken, "/getMe")
+	if err != nil {
+		return nil, fmt.Errorf("telegram: unable to join base URL and path: %w", err)
+	}
+
+	request, err := http.NewRequest(http.MethodGet, endpointURL, nil)
+	if err != nil {
+		return nil, fmt.Errorf("telegram: unable to create request: %v", err)
+	}
+
+	request.Header.Set("Accept", "application/json")
+	request.Header.Set("User-Agent", "Miniflux/"+version.Version)
+
+	httpClient := &http.Client{Timeout: defaultClientTimeout}
+	response, err := httpClient.Do(request)
+	if err != nil {
+		return nil, fmt.Errorf("telegram: unable to send request: %v", err)
+	}
+	defer response.Body.Close()
+
+	var userResponse UserResponse
+	if err := json.NewDecoder(response.Body).Decode(&userResponse); err != nil {
+		return nil, fmt.Errorf("telegram: unable to decode user response: %w", err)
+	}
+
+	if !userResponse.Ok {
+		return nil, fmt.Errorf("telegram: unable to send message: %s (error code is %d)", userResponse.Description, userResponse.ErrorCode)
+	}
+
+	return &userResponse.Result, nil
+}
+
+// Specs: https://core.telegram.org/bots/api#sendmessage
+func (c *Client) SendMessage(message *MessageRequest) (*Message, error) {
+	endpointURL, err := url.JoinPath(telegramAPIEndpoint, "/bot"+c.botToken, "/sendMessage")
+	if err != nil {
+		return nil, fmt.Errorf("telegram: unable to join base URL and path: %w", err)
+	}
+
+	requestBody, err := json.Marshal(message)
+	if err != nil {
+		return nil, fmt.Errorf("telegram: unable to encode request body: %v", err)
+	}
+
+	request, err := http.NewRequest(http.MethodPost, endpointURL, bytes.NewReader(requestBody))
+	if err != nil {
+		return nil, fmt.Errorf("telegram: unable to create request: %v", err)
+	}
+
+	request.Header.Set("Content-Type", "application/json")
+	request.Header.Set("Accept", "application/json")
+	request.Header.Set("User-Agent", "Miniflux/"+version.Version)
+
+	httpClient := &http.Client{Timeout: defaultClientTimeout}
+	response, err := httpClient.Do(request)
+	if err != nil {
+		return nil, fmt.Errorf("telegram: unable to send request: %v", err)
+	}
+	defer response.Body.Close()
+
+	var messageResponse MessageResponse
+	if err := json.NewDecoder(response.Body).Decode(&messageResponse); err != nil {
+		return nil, fmt.Errorf("telegram: unable to decode discovery response: %w", err)
+	}
+
+	if !messageResponse.Ok {
+		return nil, fmt.Errorf("telegram: unable to send message: %s (error code is %d)", messageResponse.Description, messageResponse.ErrorCode)
+	}
+
+	return &messageResponse.Result, nil
+}
+
+type InlineKeyboard struct {
+	InlineKeyboard []InlineKeyboardRow `json:"inline_keyboard"`
+}
+
+type InlineKeyboardRow []*InlineKeyboardButton
+
+type InlineKeyboardButton struct {
+	Text string `json:"text"`
+	URL  string `json:"url,omitempty"`
+}
+
+type User struct {
+	ID                      int64  `json:"id"`
+	IsBot                   bool   `json:"is_bot"`
+	FirstName               string `json:"first_name"`
+	LastName                string `json:"last_name"`
+	Username                string `json:"username"`
+	LanguageCode            string `json:"language_code"`
+	IsPremium               bool   `json:"is_premium"`
+	CanJoinGroups           bool   `json:"can_join_groups"`
+	CanReadAllGroupMessages bool   `json:"can_read_all_group_messages"`
+	SupportsInlineQueries   bool   `json:"supports_inline_queries"`
+}
+
+type Chat struct {
+	ID    int64  `json:"id"`
+	Type  string `json:"type"`
+	Title string `json:"title"`
+}
+
+type Message struct {
+	MessageID       int64 `json:"message_id"`
+	From            User  `json:"from"`
+	Chat            Chat  `json:"chat"`
+	MessageThreadID int64 `json:"message_thread_id"`
+	Date            int64 `json:"date"`
+}
+
+type BaseResponse struct {
+	Ok          bool   `json:"ok"`
+	ErrorCode   int    `json:"error_code"`
+	Description string `json:"description"`
+}
+
+type UserResponse struct {
+	BaseResponse
+	Result User `json:"result"`
+}
+
+type MessageRequest struct {
+	ChatID                string          `json:"chat_id"`
+	MessageThreadID       int64           `json:"message_thread_id,omitempty"`
+	Text                  string          `json:"text"`
+	ParseMode             string          `json:"parse_mode,omitempty"`
+	DisableWebPagePreview bool            `json:"disable_web_page_preview"`
+	DisableNotification   bool            `json:"disable_notification"`
+	ReplyMarkup           *InlineKeyboard `json:"reply_markup,omitempty"`
+}
+
+type MessageResponse struct {
+	BaseResponse
+	Result Message `json:"result"`
+}

+ 28 - 28
internal/integration/telegrambot/telegrambot.go

@@ -4,47 +4,47 @@
 package telegrambot // import "miniflux.app/v2/internal/integration/telegrambot"
 
 import (
-	"bytes"
 	"fmt"
-	"html/template"
-	"strconv"
 
-	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
 	"miniflux.app/v2/internal/model"
 )
 
-// PushEntry pushes entry to telegram chat using integration settings provided
-func PushEntry(entry *model.Entry, botToken, chatID string) error {
-	bot, err := tgbotapi.NewBotAPI(botToken)
-	if err != nil {
-		return fmt.Errorf("telegrambot: bot creation failed: %w", err)
+func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, topicID *int64, disableWebPagePreview, disableNotification bool) error {
+	textTemplate := `<b><a href=%q>%s</a></b> - <a href=%q>%s</a>`
+	formattedText := fmt.Sprintf(
+		textTemplate,
+		feed.SiteURL,
+		feed.Title,
+		entry.URL,
+		entry.Title,
+	)
+
+	message := &MessageRequest{
+		ChatID:                chatID,
+		Text:                  formattedText,
+		ParseMode:             HTMLFormatting,
+		DisableWebPagePreview: disableWebPagePreview,
+		DisableNotification:   disableNotification,
 	}
 
-	tpl, err := template.New("message").Parse("{{ .Title }}\n<a href=\"{{ .URL }}\">{{ .URL }}</a>")
-	if err != nil {
-		return fmt.Errorf("telegrambot: template parsing failed: %w", err)
+	if topicID != nil {
+		message.MessageThreadID = *topicID
 	}
 
-	var result bytes.Buffer
-	if err := tpl.Execute(&result, entry); err != nil {
-		return fmt.Errorf("telegrambot: template execution failed: %w", err)
-	}
+	var markupRow []*InlineKeyboardButton
 
-	chatIDInt, _ := strconv.ParseInt(chatID, 10, 64)
-	msg := tgbotapi.NewMessage(chatIDInt, result.String())
-	msg.ParseMode = tgbotapi.ModeHTML
-	msg.DisableWebPagePreview = false
+	minifluxURLButton := InlineKeyboardButton{Text: "Go to article", URL: entry.URL}
+	markupRow = append(markupRow, &minifluxURLButton)
 
 	if entry.CommentsURL != "" {
-		msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(
-			tgbotapi.NewInlineKeyboardRow(
-				tgbotapi.NewInlineKeyboardButtonURL("Comments", entry.CommentsURL),
-			))
+		commentButton := InlineKeyboardButton{Text: "Comments", URL: entry.CommentsURL}
+		markupRow = append(markupRow, &commentButton)
 	}
 
-	if _, err := bot.Send(msg); err != nil {
-		return fmt.Errorf("telegrambot: sending message failed: %w", err)
-	}
+	message.ReplyMarkup = &InlineKeyboard{}
+	message.ReplyMarkup.InlineKeyboard = append(message.ReplyMarkup.InlineKeyboard, markupRow)
 
-	return nil
+	client := NewClient(botToken, chatID)
+	_, err := client.SendMessage(message)
+	return err
 }

+ 3 - 0
internal/locale/translations/de_DE.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "Pushen Sie neue Artikel in den Telegram-Chat",
     "form.integration.telegram_bot_token": "Bot token",
     "form.integration.telegram_chat_id": "Chat ID",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Artikel in Linkding speichern",
     "form.integration.linkding_endpoint": "Linkding API-Endpunkt",
     "form.integration.linkding_api_key": "Linkding API-Schlüssel",

+ 3 - 0
internal/locale/translations/el_EL.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "Προωθήστε νέα άρθρα στη συνομιλία Telegram",
     "form.integration.telegram_bot_token": "Διακριτικό bot",
     "form.integration.telegram_chat_id": "Αναγνωριστικό συνομιλίας",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Αποθήκευση άρθρων στο Linkding",
     "form.integration.linkding_endpoint": "Τελικό σημείο Linkding API",
     "form.integration.linkding_api_key": "Κλειδί API Linkding",

+ 3 - 0
internal/locale/translations/en_US.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "Push new entries to Telegram chat",
     "form.integration.telegram_bot_token": "Bot token",
     "form.integration.telegram_chat_id": "Chat ID",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Save entries to Linkding",
     "form.integration.linkding_endpoint": "Linkding API Endpoint",
     "form.integration.linkding_api_key": "Linkding API key",

+ 3 - 0
internal/locale/translations/es_ES.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "Envíe nuevos artículos al chat de Telegram",
     "form.integration.telegram_bot_token": "Token de bot",
     "form.integration.telegram_chat_id": "ID de chat",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Enviar artículos a Linkding",
     "form.integration.linkding_endpoint": "Acceso API de Linkding",
     "form.integration.linkding_api_key": "Clave de API de Linkding",

+ 3 - 0
internal/locale/translations/fi_FI.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "Lähetä uusia artikkeleita Telegram-chatiin",
     "form.integration.telegram_bot_token": "Bot-tunnus",
     "form.integration.telegram_chat_id": "Chat ID",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Tallenna artikkelit Linkkiin",
     "form.integration.linkding_endpoint": "Linkding API-päätepiste",
     "form.integration.linkding_api_key": "Linkding API-avain",

+ 3 - 0
internal/locale/translations/fr_FR.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "Envoyer les nouveaux articles vers Telegram",
     "form.integration.telegram_bot_token": "Jeton de sécurité de l'API du Bot Telegram",
     "form.integration.telegram_chat_id": "Identifiant de discussion",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Désactiver l'aperçu de la page Web",
+    "form.integration.telegram_bot_disable_notification": "Désactiver les notifications",
     "form.integration.linkding_activate": "Sauvegarder les articles vers Linkding",
     "form.integration.linkding_endpoint": "URL de l'API de Linkding",
     "form.integration.linkding_api_key": "Clé d'API de Linkding",

+ 3 - 0
internal/locale/translations/hi_IN.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "टेलीग्राम चैट के लिए नई विषय-कविता पुश करें",
     "form.integration.telegram_bot_token": "बॉट टोकन",
     "form.integration.telegram_chat_id": "चैट आईडी",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "लिंक्डिन में विषयवस्तु सहेजें",
     "form.integration.linkding_endpoint": "लिंकिंग एपीआई समापन बिंदु",
     "form.integration.linkding_api_key": "लिंकिंग एपीआई कुंजी",

+ 3 - 0
internal/locale/translations/id_ID.json

@@ -369,6 +369,9 @@
     "form.integration.telegram_bot_activate": "Kirim artikel baru ke percakapan Telegram",
     "form.integration.telegram_bot_token": "Token Bot",
     "form.integration.telegram_chat_id": "ID Obrolan",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Simpan artikel ke Linkding",
     "form.integration.linkding_endpoint": "Titik URL API Linkding",
     "form.integration.linkding_api_key": "Kunci API Linkding",

+ 3 - 0
internal/locale/translations/it_IT.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "Invia nuovi articoli alla chat di Telegram",
     "form.integration.telegram_bot_token": "Token bot",
     "form.integration.telegram_chat_id": "ID chat",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Salva gli articoli su Linkding",
     "form.integration.linkding_endpoint": "Endpoint dell'API di Linkding",
     "form.integration.linkding_api_key": "API key dell'account Linkding",

+ 3 - 0
internal/locale/translations/ja_JP.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "新しい記事を Telegram チャットにプッシュする",
     "form.integration.telegram_bot_token": "ボットトークン",
     "form.integration.telegram_chat_id": "チャット ID",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Linkding に記事を保存する",
     "form.integration.linkding_endpoint": "Linkding の API Endpoint",
     "form.integration.linkding_api_key": "Linkding の API key",

+ 3 - 0
internal/locale/translations/nl_NL.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "Push nieuwe artikelen naar Telegram-chat",
     "form.integration.telegram_bot_token": "Bot token",
     "form.integration.telegram_chat_id": "Chat ID",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Opslaan naar Linkding",
     "form.integration.linkding_endpoint": "Linkding URL",
     "form.integration.linkding_api_key": "Linkding API-sleutel",

+ 3 - 0
internal/locale/translations/pl_PL.json

@@ -374,6 +374,9 @@
     "form.integration.telegram_bot_activate": "Przesyłaj nowe artykuły do czatu Telegram",
     "form.integration.telegram_bot_token": "Token bota",
     "form.integration.telegram_chat_id": "Identyfikator czatu",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Zapisz artykuły do Linkding",
     "form.integration.linkding_endpoint": "Linkding URL",
     "form.integration.linkding_api_key": "Linkding API key",

+ 3 - 0
internal/locale/translations/pt_BR.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "Envie novos artigos para o chat do Telegram",
     "form.integration.telegram_bot_token": "Token de bot",
     "form.integration.telegram_chat_id": "ID de bate-papo",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Salvar itens no Linkding",
     "form.integration.linkding_endpoint": "Endpoint de API do Linkding",
     "form.integration.linkding_api_key": "Chave de API do Linkding",

+ 3 - 0
internal/locale/translations/ru_RU.json

@@ -374,6 +374,9 @@
     "form.integration.telegram_bot_activate": "Репостить новые статьи в Telegram-чат",
     "form.integration.telegram_bot_token": "Токен бота",
     "form.integration.telegram_chat_id": "ID чата",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Сохранять статьи в Linkding",
     "form.integration.linkding_endpoint": "Конечная точка Linkding API",
     "form.integration.linkding_api_key": "API-ключ Linkding",

+ 3 - 0
internal/locale/translations/tr_TR.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "Yeni makaleleri Telegram sohbetine gönderin",
     "form.integration.telegram_bot_token": "Bot jetonu",
     "form.integration.telegram_chat_id": "Sohbet kimliği",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "Makaleleri Linkding'e kaydet",
     "form.integration.linkding_endpoint": "Linkding API Uç Noktası",
     "form.integration.linkding_api_key": "Linkding API Anahtarı",

+ 3 - 0
internal/locale/translations/uk_UA.json

@@ -374,6 +374,9 @@
   "form.integration.readwise_api_key_link": "Get your Readwise Access Token",
   "form.integration.telegram_bot_activate": "Відправляти нові статті до чату Telegram",
   "form.integration.telegram_bot_token": "Токен боту",
+  "form.integration.telegram_topic_id": "Topic ID",
+  "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+  "form.integration.telegram_bot_disable_notification": "Disable notification",
   "form.integration.telegram_chat_id": "ID чату",
   "form.integration.linkding_activate": "Зберігати статті до Linkding",
   "form.integration.linkding_endpoint": "Linkding API Endpoint",

+ 3 - 0
internal/locale/translations/zh_CN.json

@@ -369,6 +369,9 @@
     "form.integration.readwise_api_key_link": "Get your Readwise Access Token",
     "form.integration.telegram_bot_activate": "将新文章推送到 Telegram",
     "form.integration.telegram_bot_token": "机器人令牌",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.telegram_chat_id": "聊天ID",
     "form.integration.linkding_activate": "保存文章到 Linkding",
     "form.integration.linkding_endpoint": "Linkding API 端点",

+ 3 - 0
internal/locale/translations/zh_TW.json

@@ -372,6 +372,9 @@
     "form.integration.telegram_bot_activate": "將新文章推送到 Telegram",
     "form.integration.telegram_bot_token": "Bot token",
     "form.integration.telegram_chat_id": "Chat ID",
+    "form.integration.telegram_topic_id": "Topic ID",
+    "form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
+    "form.integration.telegram_bot_disable_notification": "Disable notification",
     "form.integration.linkding_activate": "儲存文章到 Linkding",
     "form.integration.linkding_endpoint": "Linkding API 端點",
     "form.integration.linkding_api_key": "Linkding API 金鑰",

+ 65 - 62
internal/model/integration.go

@@ -5,66 +5,69 @@ package model // import "miniflux.app/v2/internal/model"
 
 // Integration represents user integration settings.
 type Integration struct {
-	UserID               int64
-	PinboardEnabled      bool
-	PinboardToken        string
-	PinboardTags         string
-	PinboardMarkAsUnread bool
-	InstapaperEnabled    bool
-	InstapaperUsername   string
-	InstapaperPassword   string
-	FeverEnabled         bool
-	FeverUsername        string
-	FeverToken           string
-	GoogleReaderEnabled  bool
-	GoogleReaderUsername string
-	GoogleReaderPassword string
-	WallabagEnabled      bool
-	WallabagOnlyURL      bool
-	WallabagURL          string
-	WallabagClientID     string
-	WallabagClientSecret string
-	WallabagUsername     string
-	WallabagPassword     string
-	NunuxKeeperEnabled   bool
-	NunuxKeeperURL       string
-	NunuxKeeperAPIKey    string
-	NotionEnabled        bool
-	NotionToken          string
-	NotionPageID         string
-	EspialEnabled        bool
-	EspialURL            string
-	EspialAPIKey         string
-	EspialTags           string
-	ReadwiseEnabled      bool
-	ReadwiseAPIKey       string
-	PocketEnabled        bool
-	PocketAccessToken    string
-	PocketConsumerKey    string
-	TelegramBotEnabled   bool
-	TelegramBotToken     string
-	TelegramBotChatID    string
-	LinkdingEnabled      bool
-	LinkdingURL          string
-	LinkdingAPIKey       string
-	LinkdingTags         string
-	LinkdingMarkAsUnread bool
-	MatrixBotEnabled     bool
-	MatrixBotUser        string
-	MatrixBotPassword    string
-	MatrixBotURL         string
-	MatrixBotChatID      string
-	AppriseEnabled       bool
-	AppriseURL           string
-	AppriseServicesURL   string
-	ShioriEnabled        bool
-	ShioriURL            string
-	ShioriUsername       string
-	ShioriPassword       string
-	ShaarliEnabled       bool
-	ShaarliURL           string
-	ShaarliAPISecret     string
-	WebhookEnabled       bool
-	WebhookURL           string
-	WebhookSecret        string
+	UserID                           int64
+	PinboardEnabled                  bool
+	PinboardToken                    string
+	PinboardTags                     string
+	PinboardMarkAsUnread             bool
+	InstapaperEnabled                bool
+	InstapaperUsername               string
+	InstapaperPassword               string
+	FeverEnabled                     bool
+	FeverUsername                    string
+	FeverToken                       string
+	GoogleReaderEnabled              bool
+	GoogleReaderUsername             string
+	GoogleReaderPassword             string
+	WallabagEnabled                  bool
+	WallabagOnlyURL                  bool
+	WallabagURL                      string
+	WallabagClientID                 string
+	WallabagClientSecret             string
+	WallabagUsername                 string
+	WallabagPassword                 string
+	NunuxKeeperEnabled               bool
+	NunuxKeeperURL                   string
+	NunuxKeeperAPIKey                string
+	NotionEnabled                    bool
+	NotionToken                      string
+	NotionPageID                     string
+	EspialEnabled                    bool
+	EspialURL                        string
+	EspialAPIKey                     string
+	EspialTags                       string
+	ReadwiseEnabled                  bool
+	ReadwiseAPIKey                   string
+	PocketEnabled                    bool
+	PocketAccessToken                string
+	PocketConsumerKey                string
+	TelegramBotEnabled               bool
+	TelegramBotToken                 string
+	TelegramBotChatID                string
+	TelegramBotTopicID               *int64
+	TelegramBotDisableWebPagePreview bool
+	TelegramBotDisableNotification   bool
+	LinkdingEnabled                  bool
+	LinkdingURL                      string
+	LinkdingAPIKey                   string
+	LinkdingTags                     string
+	LinkdingMarkAsUnread             bool
+	MatrixBotEnabled                 bool
+	MatrixBotUser                    string
+	MatrixBotPassword                string
+	MatrixBotURL                     string
+	MatrixBotChatID                  string
+	AppriseEnabled                   bool
+	AppriseURL                       string
+	AppriseServicesURL               string
+	ShioriEnabled                    bool
+	ShioriURL                        string
+	ShioriUsername                   string
+	ShioriPassword                   string
+	ShaarliEnabled                   bool
+	ShaarliURL                       string
+	ShaarliAPISecret                 string
+	WebhookEnabled                   bool
+	WebhookURL                       string
+	WebhookSecret                    string
 }

+ 45 - 33
internal/storage/integration.go

@@ -148,6 +148,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			telegram_bot_enabled,
 			telegram_bot_token,
 			telegram_bot_chat_id,
+			telegram_bot_topic_id,
+			telegram_bot_disable_web_page_preview,
+			telegram_bot_disable_notification,
 			linkding_enabled,
 			linkding_url,
 			linkding_api_key,
@@ -217,6 +220,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.TelegramBotEnabled,
 		&integration.TelegramBotToken,
 		&integration.TelegramBotChatID,
+		&integration.TelegramBotTopicID,
+		&integration.TelegramBotDisableWebPagePreview,
+		&integration.TelegramBotDisableNotification,
 		&integration.LinkdingEnabled,
 		&integration.LinkdingURL,
 		&integration.LinkdingAPIKey,
@@ -286,40 +292,43 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			telegram_bot_enabled=$27,
 			telegram_bot_token=$28,
 			telegram_bot_chat_id=$29,
-			espial_enabled=$30,
-			espial_url=$31,
-			espial_api_key=$32,
-			espial_tags=$33,
-			linkding_enabled=$34,
-			linkding_url=$35,
-			linkding_api_key=$36,
-			linkding_tags=$37,
-			linkding_mark_as_unread=$38,
-			matrix_bot_enabled=$39,
-			matrix_bot_user=$40,
-			matrix_bot_password=$41,
-			matrix_bot_url=$42,
-			matrix_bot_chat_id=$43,
-			notion_enabled=$44,
-			notion_token=$45,
-			notion_page_id=$46,
-			readwise_enabled=$47,
-			readwise_api_key=$48,
-			apprise_enabled=$49,
-			apprise_url=$50,
-			apprise_services_url=$51,
-			shiori_enabled=$52,
-			shiori_url=$53,
-			shiori_username=$54,
-			shiori_password=$55,
-			shaarli_enabled=$56,
-			shaarli_url=$57,
-			shaarli_api_secret=$58,
-			webhook_enabled=$59,
-			webhook_url=$60,
-			webhook_secret=$61
+			telegram_bot_topic_id=$30,
+			telegram_bot_disable_web_page_preview=$31,
+			telegram_bot_disable_notification=$32,
+			espial_enabled=$33,
+			espial_url=$34,
+			espial_api_key=$35,
+			espial_tags=$36,
+			linkding_enabled=$37,
+			linkding_url=$38,
+			linkding_api_key=$39,
+			linkding_tags=$40,
+			linkding_mark_as_unread=$41,
+			matrix_bot_enabled=$42,
+			matrix_bot_user=$43,
+			matrix_bot_password=$44,
+			matrix_bot_url=$45,
+			matrix_bot_chat_id=$46,
+			notion_enabled=$47,
+			notion_token=$48,
+			notion_page_id=$49,
+			readwise_enabled=$50,
+			readwise_api_key=$51,
+			apprise_enabled=$52,
+			apprise_url=$53,
+			apprise_services_url=$54,
+			shiori_enabled=$55,
+			shiori_url=$56,
+			shiori_username=$57,
+			shiori_password=$58,
+			shaarli_enabled=$59,
+			shaarli_url=$60,
+			shaarli_api_secret=$61,
+			webhook_enabled=$62,
+			webhook_url=$63,
+			webhook_secret=$64
 		WHERE
-			user_id=$62
+			user_id=$65
 	`
 	_, err := s.db.Exec(
 		query,
@@ -352,6 +361,9 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.TelegramBotEnabled,
 		integration.TelegramBotToken,
 		integration.TelegramBotChatID,
+		integration.TelegramBotTopicID,
+		integration.TelegramBotDisableWebPagePreview,
+		integration.TelegramBotDisableNotification,
 		integration.EspialEnabled,
 		integration.EspialURL,
 		integration.EspialAPIKey,

+ 11 - 0
internal/template/templates/views/integrations.html

@@ -328,6 +328,17 @@
             <label for="form-telegram-chat-id">{{ t "form.integration.telegram_chat_id" }}</label>
             <input type="text" name="telegram_bot_chat_id" id="form-telegram-chat-id" value="{{ .form.TelegramBotChatID }}" spellcheck="false">
 
+            <label for="form-telegram-topic-id">{{ t "form.integration.telegram_topic_id" }}</label>
+            <input type="number" name="telegram_bot_topic_id" id="form-telegram-topic-id" {{ if .form.TelegramBotTopicID }}value="{{ .form.TelegramBotTopicID }}"{{ end }}>
+
+            <label>
+                <input type="checkbox" name="telegram_bot_disable_web_page_preview" value="1" {{ if .form.TelegramBotDisableWebPagePreview }}checked{{ end }}> {{ t "form.integration.telegram_bot_disable_web_page_preview" }}
+            </label>
+
+            <label>
+                <input type="checkbox" name="telegram_bot_disable_notification" value="1" {{ if .form.TelegramBotDisableNotification }}checked{{ end }}> {{ t "form.integration.telegram_bot_disable_notification" }}
+            </label>
+
             <div class="buttons">
                 <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
             </div>

+ 139 - 121
internal/ui/form/integration.go

@@ -5,73 +5,77 @@ package form // import "miniflux.app/v2/internal/ui/form"
 
 import (
 	"net/http"
+	"strconv"
 
 	"miniflux.app/v2/internal/model"
 )
 
 // IntegrationForm represents user integration settings form.
 type IntegrationForm struct {
-	PinboardEnabled      bool
-	PinboardToken        string
-	PinboardTags         string
-	PinboardMarkAsUnread bool
-	InstapaperEnabled    bool
-	InstapaperUsername   string
-	InstapaperPassword   string
-	FeverEnabled         bool
-	FeverUsername        string
-	FeverPassword        string
-	GoogleReaderEnabled  bool
-	GoogleReaderUsername string
-	GoogleReaderPassword string
-	WallabagEnabled      bool
-	WallabagOnlyURL      bool
-	WallabagURL          string
-	WallabagClientID     string
-	WallabagClientSecret string
-	WallabagUsername     string
-	WallabagPassword     string
-	NotionEnabled        bool
-	NotionPageID         string
-	NotionToken          string
-	NunuxKeeperEnabled   bool
-	NunuxKeeperURL       string
-	NunuxKeeperAPIKey    string
-	EspialEnabled        bool
-	EspialURL            string
-	EspialAPIKey         string
-	EspialTags           string
-	ReadwiseEnabled      bool
-	ReadwiseAPIKey       string
-	PocketEnabled        bool
-	PocketAccessToken    string
-	PocketConsumerKey    string
-	TelegramBotEnabled   bool
-	TelegramBotToken     string
-	TelegramBotChatID    string
-	LinkdingEnabled      bool
-	LinkdingURL          string
-	LinkdingAPIKey       string
-	LinkdingTags         string
-	LinkdingMarkAsUnread bool
-	MatrixBotEnabled     bool
-	MatrixBotUser        string
-	MatrixBotPassword    string
-	MatrixBotURL         string
-	MatrixBotChatID      string
-	AppriseEnabled       bool
-	AppriseURL           string
-	AppriseServicesURL   string
-	ShioriEnabled        bool
-	ShioriURL            string
-	ShioriUsername       string
-	ShioriPassword       string
-	ShaarliEnabled       bool
-	ShaarliURL           string
-	ShaarliAPISecret     string
-	WebhookEnabled       bool
-	WebhookURL           string
-	WebhookSecret        string
+	PinboardEnabled                  bool
+	PinboardToken                    string
+	PinboardTags                     string
+	PinboardMarkAsUnread             bool
+	InstapaperEnabled                bool
+	InstapaperUsername               string
+	InstapaperPassword               string
+	FeverEnabled                     bool
+	FeverUsername                    string
+	FeverPassword                    string
+	GoogleReaderEnabled              bool
+	GoogleReaderUsername             string
+	GoogleReaderPassword             string
+	WallabagEnabled                  bool
+	WallabagOnlyURL                  bool
+	WallabagURL                      string
+	WallabagClientID                 string
+	WallabagClientSecret             string
+	WallabagUsername                 string
+	WallabagPassword                 string
+	NotionEnabled                    bool
+	NotionPageID                     string
+	NotionToken                      string
+	NunuxKeeperEnabled               bool
+	NunuxKeeperURL                   string
+	NunuxKeeperAPIKey                string
+	EspialEnabled                    bool
+	EspialURL                        string
+	EspialAPIKey                     string
+	EspialTags                       string
+	ReadwiseEnabled                  bool
+	ReadwiseAPIKey                   string
+	PocketEnabled                    bool
+	PocketAccessToken                string
+	PocketConsumerKey                string
+	TelegramBotEnabled               bool
+	TelegramBotToken                 string
+	TelegramBotChatID                string
+	TelegramBotTopicID               *int64
+	TelegramBotDisableWebPagePreview bool
+	TelegramBotDisableNotification   bool
+	LinkdingEnabled                  bool
+	LinkdingURL                      string
+	LinkdingAPIKey                   string
+	LinkdingTags                     string
+	LinkdingMarkAsUnread             bool
+	MatrixBotEnabled                 bool
+	MatrixBotUser                    string
+	MatrixBotPassword                string
+	MatrixBotURL                     string
+	MatrixBotChatID                  string
+	AppriseEnabled                   bool
+	AppriseURL                       string
+	AppriseServicesURL               string
+	ShioriEnabled                    bool
+	ShioriURL                        string
+	ShioriUsername                   string
+	ShioriPassword                   string
+	ShaarliEnabled                   bool
+	ShaarliURL                       string
+	ShaarliAPISecret                 string
+	WebhookEnabled                   bool
+	WebhookURL                       string
+	WebhookSecret                    string
 }
 
 // Merge copy form values to the model.
@@ -112,6 +116,9 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.TelegramBotEnabled = i.TelegramBotEnabled
 	integration.TelegramBotToken = i.TelegramBotToken
 	integration.TelegramBotChatID = i.TelegramBotChatID
+	integration.TelegramBotTopicID = i.TelegramBotTopicID
+	integration.TelegramBotDisableWebPagePreview = i.TelegramBotDisableWebPagePreview
+	integration.TelegramBotDisableNotification = i.TelegramBotDisableNotification
 	integration.LinkdingEnabled = i.LinkdingEnabled
 	integration.LinkdingURL = i.LinkdingURL
 	integration.LinkdingAPIKey = i.LinkdingAPIKey
@@ -139,65 +146,76 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 // NewIntegrationForm returns a new IntegrationForm.
 func NewIntegrationForm(r *http.Request) *IntegrationForm {
 	return &IntegrationForm{
-		PinboardEnabled:      r.FormValue("pinboard_enabled") == "1",
-		PinboardToken:        r.FormValue("pinboard_token"),
-		PinboardTags:         r.FormValue("pinboard_tags"),
-		PinboardMarkAsUnread: r.FormValue("pinboard_mark_as_unread") == "1",
-		InstapaperEnabled:    r.FormValue("instapaper_enabled") == "1",
-		InstapaperUsername:   r.FormValue("instapaper_username"),
-		InstapaperPassword:   r.FormValue("instapaper_password"),
-		FeverEnabled:         r.FormValue("fever_enabled") == "1",
-		FeverUsername:        r.FormValue("fever_username"),
-		FeverPassword:        r.FormValue("fever_password"),
-		GoogleReaderEnabled:  r.FormValue("googlereader_enabled") == "1",
-		GoogleReaderUsername: r.FormValue("googlereader_username"),
-		GoogleReaderPassword: r.FormValue("googlereader_password"),
-		WallabagEnabled:      r.FormValue("wallabag_enabled") == "1",
-		WallabagOnlyURL:      r.FormValue("wallabag_only_url") == "1",
-		WallabagURL:          r.FormValue("wallabag_url"),
-		WallabagClientID:     r.FormValue("wallabag_client_id"),
-		WallabagClientSecret: r.FormValue("wallabag_client_secret"),
-		WallabagUsername:     r.FormValue("wallabag_username"),
-		WallabagPassword:     r.FormValue("wallabag_password"),
-		NotionEnabled:        r.FormValue("notion_enabled") == "1",
-		NotionPageID:         r.FormValue("notion_page_id"),
-		NotionToken:          r.FormValue("notion_token"),
-		NunuxKeeperEnabled:   r.FormValue("nunux_keeper_enabled") == "1",
-		NunuxKeeperURL:       r.FormValue("nunux_keeper_url"),
-		NunuxKeeperAPIKey:    r.FormValue("nunux_keeper_api_key"),
-		EspialEnabled:        r.FormValue("espial_enabled") == "1",
-		EspialURL:            r.FormValue("espial_url"),
-		EspialAPIKey:         r.FormValue("espial_api_key"),
-		EspialTags:           r.FormValue("espial_tags"),
-		ReadwiseEnabled:      r.FormValue("readwise_enabled") == "1",
-		ReadwiseAPIKey:       r.FormValue("readwise_api_key"),
-		PocketEnabled:        r.FormValue("pocket_enabled") == "1",
-		PocketAccessToken:    r.FormValue("pocket_access_token"),
-		PocketConsumerKey:    r.FormValue("pocket_consumer_key"),
-		TelegramBotEnabled:   r.FormValue("telegram_bot_enabled") == "1",
-		TelegramBotToken:     r.FormValue("telegram_bot_token"),
-		TelegramBotChatID:    r.FormValue("telegram_bot_chat_id"),
-		LinkdingEnabled:      r.FormValue("linkding_enabled") == "1",
-		LinkdingURL:          r.FormValue("linkding_url"),
-		LinkdingAPIKey:       r.FormValue("linkding_api_key"),
-		LinkdingTags:         r.FormValue("linkding_tags"),
-		LinkdingMarkAsUnread: r.FormValue("linkding_mark_as_unread") == "1",
-		MatrixBotEnabled:     r.FormValue("matrix_bot_enabled") == "1",
-		MatrixBotUser:        r.FormValue("matrix_bot_user"),
-		MatrixBotPassword:    r.FormValue("matrix_bot_password"),
-		MatrixBotURL:         r.FormValue("matrix_bot_url"),
-		MatrixBotChatID:      r.FormValue("matrix_bot_chat_id"),
-		AppriseEnabled:       r.FormValue("apprise_enabled") == "1",
-		AppriseURL:           r.FormValue("apprise_url"),
-		AppriseServicesURL:   r.FormValue("apprise_services_url"),
-		ShioriEnabled:        r.FormValue("shiori_enabled") == "1",
-		ShioriURL:            r.FormValue("shiori_url"),
-		ShioriUsername:       r.FormValue("shiori_username"),
-		ShioriPassword:       r.FormValue("shiori_password"),
-		ShaarliEnabled:       r.FormValue("shaarli_enabled") == "1",
-		ShaarliURL:           r.FormValue("shaarli_url"),
-		ShaarliAPISecret:     r.FormValue("shaarli_api_secret"),
-		WebhookEnabled:       r.FormValue("webhook_enabled") == "1",
-		WebhookURL:           r.FormValue("webhook_url"),
+		PinboardEnabled:                  r.FormValue("pinboard_enabled") == "1",
+		PinboardToken:                    r.FormValue("pinboard_token"),
+		PinboardTags:                     r.FormValue("pinboard_tags"),
+		PinboardMarkAsUnread:             r.FormValue("pinboard_mark_as_unread") == "1",
+		InstapaperEnabled:                r.FormValue("instapaper_enabled") == "1",
+		InstapaperUsername:               r.FormValue("instapaper_username"),
+		InstapaperPassword:               r.FormValue("instapaper_password"),
+		FeverEnabled:                     r.FormValue("fever_enabled") == "1",
+		FeverUsername:                    r.FormValue("fever_username"),
+		FeverPassword:                    r.FormValue("fever_password"),
+		GoogleReaderEnabled:              r.FormValue("googlereader_enabled") == "1",
+		GoogleReaderUsername:             r.FormValue("googlereader_username"),
+		GoogleReaderPassword:             r.FormValue("googlereader_password"),
+		WallabagEnabled:                  r.FormValue("wallabag_enabled") == "1",
+		WallabagOnlyURL:                  r.FormValue("wallabag_only_url") == "1",
+		WallabagURL:                      r.FormValue("wallabag_url"),
+		WallabagClientID:                 r.FormValue("wallabag_client_id"),
+		WallabagClientSecret:             r.FormValue("wallabag_client_secret"),
+		WallabagUsername:                 r.FormValue("wallabag_username"),
+		WallabagPassword:                 r.FormValue("wallabag_password"),
+		NotionEnabled:                    r.FormValue("notion_enabled") == "1",
+		NotionPageID:                     r.FormValue("notion_page_id"),
+		NotionToken:                      r.FormValue("notion_token"),
+		NunuxKeeperEnabled:               r.FormValue("nunux_keeper_enabled") == "1",
+		NunuxKeeperURL:                   r.FormValue("nunux_keeper_url"),
+		NunuxKeeperAPIKey:                r.FormValue("nunux_keeper_api_key"),
+		EspialEnabled:                    r.FormValue("espial_enabled") == "1",
+		EspialURL:                        r.FormValue("espial_url"),
+		EspialAPIKey:                     r.FormValue("espial_api_key"),
+		EspialTags:                       r.FormValue("espial_tags"),
+		ReadwiseEnabled:                  r.FormValue("readwise_enabled") == "1",
+		ReadwiseAPIKey:                   r.FormValue("readwise_api_key"),
+		PocketEnabled:                    r.FormValue("pocket_enabled") == "1",
+		PocketAccessToken:                r.FormValue("pocket_access_token"),
+		PocketConsumerKey:                r.FormValue("pocket_consumer_key"),
+		TelegramBotEnabled:               r.FormValue("telegram_bot_enabled") == "1",
+		TelegramBotToken:                 r.FormValue("telegram_bot_token"),
+		TelegramBotChatID:                r.FormValue("telegram_bot_chat_id"),
+		TelegramBotTopicID:               optionalInt64Field(r.FormValue("telegram_bot_topic_id")),
+		TelegramBotDisableWebPagePreview: r.FormValue("telegram_bot_disable_web_page_preview") == "1",
+		TelegramBotDisableNotification:   r.FormValue("telegram_bot_disable_notification") == "1",
+		LinkdingEnabled:                  r.FormValue("linkding_enabled") == "1",
+		LinkdingURL:                      r.FormValue("linkding_url"),
+		LinkdingAPIKey:                   r.FormValue("linkding_api_key"),
+		LinkdingTags:                     r.FormValue("linkding_tags"),
+		LinkdingMarkAsUnread:             r.FormValue("linkding_mark_as_unread") == "1",
+		MatrixBotEnabled:                 r.FormValue("matrix_bot_enabled") == "1",
+		MatrixBotUser:                    r.FormValue("matrix_bot_user"),
+		MatrixBotPassword:                r.FormValue("matrix_bot_password"),
+		MatrixBotURL:                     r.FormValue("matrix_bot_url"),
+		MatrixBotChatID:                  r.FormValue("matrix_bot_chat_id"),
+		AppriseEnabled:                   r.FormValue("apprise_enabled") == "1",
+		AppriseURL:                       r.FormValue("apprise_url"),
+		AppriseServicesURL:               r.FormValue("apprise_services_url"),
+		ShioriEnabled:                    r.FormValue("shiori_enabled") == "1",
+		ShioriURL:                        r.FormValue("shiori_url"),
+		ShioriUsername:                   r.FormValue("shiori_username"),
+		ShioriPassword:                   r.FormValue("shiori_password"),
+		ShaarliEnabled:                   r.FormValue("shaarli_enabled") == "1",
+		ShaarliURL:                       r.FormValue("shaarli_url"),
+		ShaarliAPISecret:                 r.FormValue("shaarli_api_secret"),
+		WebhookEnabled:                   r.FormValue("webhook_enabled") == "1",
+		WebhookURL:                       r.FormValue("webhook_url"),
 	}
 }
+
+func optionalInt64Field(formValue string) *int64 {
+	if formValue == "" {
+		return nil
+	}
+	value, _ := strconv.ParseInt(formValue, 10, 64)
+	return &value
+}

+ 62 - 59
internal/ui/integration_show.go

@@ -28,65 +28,68 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
 	}
 
 	integrationForm := form.IntegrationForm{
-		PinboardEnabled:      integration.PinboardEnabled,
-		PinboardToken:        integration.PinboardToken,
-		PinboardTags:         integration.PinboardTags,
-		PinboardMarkAsUnread: integration.PinboardMarkAsUnread,
-		InstapaperEnabled:    integration.InstapaperEnabled,
-		InstapaperUsername:   integration.InstapaperUsername,
-		InstapaperPassword:   integration.InstapaperPassword,
-		FeverEnabled:         integration.FeverEnabled,
-		FeverUsername:        integration.FeverUsername,
-		GoogleReaderEnabled:  integration.GoogleReaderEnabled,
-		GoogleReaderUsername: integration.GoogleReaderUsername,
-		WallabagEnabled:      integration.WallabagEnabled,
-		WallabagOnlyURL:      integration.WallabagOnlyURL,
-		WallabagURL:          integration.WallabagURL,
-		WallabagClientID:     integration.WallabagClientID,
-		WallabagClientSecret: integration.WallabagClientSecret,
-		WallabagUsername:     integration.WallabagUsername,
-		WallabagPassword:     integration.WallabagPassword,
-		NotionEnabled:        integration.NotionEnabled,
-		NotionPageID:         integration.NotionPageID,
-		NotionToken:          integration.NotionToken,
-		NunuxKeeperEnabled:   integration.NunuxKeeperEnabled,
-		NunuxKeeperURL:       integration.NunuxKeeperURL,
-		NunuxKeeperAPIKey:    integration.NunuxKeeperAPIKey,
-		EspialEnabled:        integration.EspialEnabled,
-		EspialURL:            integration.EspialURL,
-		EspialAPIKey:         integration.EspialAPIKey,
-		EspialTags:           integration.EspialTags,
-		ReadwiseEnabled:      integration.ReadwiseEnabled,
-		ReadwiseAPIKey:       integration.ReadwiseAPIKey,
-		PocketEnabled:        integration.PocketEnabled,
-		PocketAccessToken:    integration.PocketAccessToken,
-		PocketConsumerKey:    integration.PocketConsumerKey,
-		TelegramBotEnabled:   integration.TelegramBotEnabled,
-		TelegramBotToken:     integration.TelegramBotToken,
-		TelegramBotChatID:    integration.TelegramBotChatID,
-		LinkdingEnabled:      integration.LinkdingEnabled,
-		LinkdingURL:          integration.LinkdingURL,
-		LinkdingAPIKey:       integration.LinkdingAPIKey,
-		LinkdingTags:         integration.LinkdingTags,
-		LinkdingMarkAsUnread: integration.LinkdingMarkAsUnread,
-		MatrixBotEnabled:     integration.MatrixBotEnabled,
-		MatrixBotUser:        integration.MatrixBotUser,
-		MatrixBotPassword:    integration.MatrixBotPassword,
-		MatrixBotURL:         integration.MatrixBotURL,
-		MatrixBotChatID:      integration.MatrixBotChatID,
-		AppriseEnabled:       integration.AppriseEnabled,
-		AppriseURL:           integration.AppriseURL,
-		AppriseServicesURL:   integration.AppriseServicesURL,
-		ShioriEnabled:        integration.ShioriEnabled,
-		ShioriURL:            integration.ShioriURL,
-		ShioriUsername:       integration.ShioriUsername,
-		ShioriPassword:       integration.ShioriPassword,
-		ShaarliEnabled:       integration.ShaarliEnabled,
-		ShaarliURL:           integration.ShaarliURL,
-		ShaarliAPISecret:     integration.ShaarliAPISecret,
-		WebhookEnabled:       integration.WebhookEnabled,
-		WebhookURL:           integration.WebhookURL,
-		WebhookSecret:        integration.WebhookSecret,
+		PinboardEnabled:                  integration.PinboardEnabled,
+		PinboardToken:                    integration.PinboardToken,
+		PinboardTags:                     integration.PinboardTags,
+		PinboardMarkAsUnread:             integration.PinboardMarkAsUnread,
+		InstapaperEnabled:                integration.InstapaperEnabled,
+		InstapaperUsername:               integration.InstapaperUsername,
+		InstapaperPassword:               integration.InstapaperPassword,
+		FeverEnabled:                     integration.FeverEnabled,
+		FeverUsername:                    integration.FeverUsername,
+		GoogleReaderEnabled:              integration.GoogleReaderEnabled,
+		GoogleReaderUsername:             integration.GoogleReaderUsername,
+		WallabagEnabled:                  integration.WallabagEnabled,
+		WallabagOnlyURL:                  integration.WallabagOnlyURL,
+		WallabagURL:                      integration.WallabagURL,
+		WallabagClientID:                 integration.WallabagClientID,
+		WallabagClientSecret:             integration.WallabagClientSecret,
+		WallabagUsername:                 integration.WallabagUsername,
+		WallabagPassword:                 integration.WallabagPassword,
+		NotionEnabled:                    integration.NotionEnabled,
+		NotionPageID:                     integration.NotionPageID,
+		NotionToken:                      integration.NotionToken,
+		NunuxKeeperEnabled:               integration.NunuxKeeperEnabled,
+		NunuxKeeperURL:                   integration.NunuxKeeperURL,
+		NunuxKeeperAPIKey:                integration.NunuxKeeperAPIKey,
+		EspialEnabled:                    integration.EspialEnabled,
+		EspialURL:                        integration.EspialURL,
+		EspialAPIKey:                     integration.EspialAPIKey,
+		EspialTags:                       integration.EspialTags,
+		ReadwiseEnabled:                  integration.ReadwiseEnabled,
+		ReadwiseAPIKey:                   integration.ReadwiseAPIKey,
+		PocketEnabled:                    integration.PocketEnabled,
+		PocketAccessToken:                integration.PocketAccessToken,
+		PocketConsumerKey:                integration.PocketConsumerKey,
+		TelegramBotEnabled:               integration.TelegramBotEnabled,
+		TelegramBotToken:                 integration.TelegramBotToken,
+		TelegramBotChatID:                integration.TelegramBotChatID,
+		TelegramBotTopicID:               integration.TelegramBotTopicID,
+		TelegramBotDisableWebPagePreview: integration.TelegramBotDisableWebPagePreview,
+		TelegramBotDisableNotification:   integration.TelegramBotDisableNotification,
+		LinkdingEnabled:                  integration.LinkdingEnabled,
+		LinkdingURL:                      integration.LinkdingURL,
+		LinkdingAPIKey:                   integration.LinkdingAPIKey,
+		LinkdingTags:                     integration.LinkdingTags,
+		LinkdingMarkAsUnread:             integration.LinkdingMarkAsUnread,
+		MatrixBotEnabled:                 integration.MatrixBotEnabled,
+		MatrixBotUser:                    integration.MatrixBotUser,
+		MatrixBotPassword:                integration.MatrixBotPassword,
+		MatrixBotURL:                     integration.MatrixBotURL,
+		MatrixBotChatID:                  integration.MatrixBotChatID,
+		AppriseEnabled:                   integration.AppriseEnabled,
+		AppriseURL:                       integration.AppriseURL,
+		AppriseServicesURL:               integration.AppriseServicesURL,
+		ShioriEnabled:                    integration.ShioriEnabled,
+		ShioriURL:                        integration.ShioriURL,
+		ShioriUsername:                   integration.ShioriUsername,
+		ShioriPassword:                   integration.ShioriPassword,
+		ShaarliEnabled:                   integration.ShaarliEnabled,
+		ShaarliURL:                       integration.ShaarliURL,
+		ShaarliAPISecret:                 integration.ShaarliAPISecret,
+		WebhookEnabled:                   integration.WebhookEnabled,
+		WebhookURL:                       integration.WebhookURL,
+		WebhookSecret:                    integration.WebhookSecret,
 	}
 
 	sess := session.New(h.store, request.SessionID(r))