diff options
Diffstat (limited to 'internal/integration')
-rw-r--r-- | internal/integration/integration.go | 23 | ||||
-rw-r--r-- | internal/integration/telegrambot/client.go | 174 | ||||
-rw-r--r-- | internal/integration/telegrambot/telegrambot.go | 56 |
3 files changed, 217 insertions, 36 deletions
diff --git a/internal/integration/integration.go b/internal/integration/integration.go index fbb3f3dc..eb9f842e 100644 --- a/internal/integration/integration.go +++ b/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) } } } diff --git a/internal/integration/telegrambot/client.go b/internal/integration/telegrambot/client.go new file mode 100644 index 00000000..ba7cedd2 --- /dev/null +++ b/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"` +} diff --git a/internal/integration/telegrambot/telegrambot.go b/internal/integration/telegrambot/telegrambot.go index a89092b5..b6097b2b 100644 --- a/internal/integration/telegrambot/telegrambot.go +++ b/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 } |