Browse Source

Add Telegram integration

三三 4 years ago
parent
commit
34dd358eb0

+ 9 - 0
database/migrations.go

@@ -546,4 +546,13 @@ var migrations = []func(tx *sql.Tx) error{
 		`)
 		return err
 	},
+	func(tx *sql.Tx) (err error) {
+		sql := `
+			ALTER TABLE integrations ADD COLUMN telegram_bot_enabled bool default 'f';
+			ALTER TABLE integrations ADD COLUMN telegram_bot_token text default '';
+			ALTER TABLE integrations ADD COLUMN telegram_bot_chat_id text default '';
+		`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 2 - 0
go.mod

@@ -6,6 +6,7 @@ require (
 	github.com/PuerkitoBio/goquery v1.7.1
 	github.com/coreos/go-oidc v2.2.1+incompatible
 	github.com/felixge/httpsnoop v1.0.1 // indirect
+	github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
 	github.com/golang/gddo v0.0.0-20200831202555-721e228c7686 // indirect
 	github.com/gorilla/mux v1.8.0
 	github.com/lib/pq v1.10.2
@@ -15,6 +16,7 @@ require (
 	github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d
 	github.com/stretchr/testify v1.6.1 // indirect
 	github.com/tdewolff/minify/v2 v2.9.21
+	github.com/technoweenie/multipartstreamer v1.0.1 // indirect
 	golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
 	golang.org/x/net v0.0.0-20210614182718-04defd469f4e
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d

+ 4 - 0
go.sum

@@ -61,6 +61,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
 github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
@@ -227,6 +229,8 @@ github.com/tdewolff/parse/v2 v2.5.19 h1:Kjaj3KQOx/4elIxlBSglus4E2oMfdROphvbq2b+O
 github.com/tdewolff/parse/v2 v2.5.19/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
 github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
 github.com/tdewolff/test v1.0.6/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/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=

+ 13 - 0
integration/integration.go

@@ -10,6 +10,7 @@ import (
 	"miniflux.app/integration/nunuxkeeper"
 	"miniflux.app/integration/pinboard"
 	"miniflux.app/integration/pocket"
+	"miniflux.app/integration/telegrambot"
 	"miniflux.app/integration/wallabag"
 	"miniflux.app/logger"
 	"miniflux.app/model"
@@ -70,3 +71,15 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
 		}
 	}
 }
+
+// PushEntry pushes new entry to the activated providers.
+// This function should be wrapped in a goroutine to avoid block of program execution.
+func PushEntry(entry *model.Entry, integration *model.Integration) {
+	if integration.TelegramBotEnabled {
+		logger.Debug("[Integration] Sending Entry #%d for User #%d to telegram", entry.ID, integration.UserID)
+		err := telegrambot.PushEntry(entry, integration.TelegramBotToken, integration.TelegramBotChatID)
+		if err != nil {
+			logger.Error("[Integration] push entry to telegram bot failed: %v", err)
+		}
+	}
+}

+ 2 - 0
integration/telegrambot/doc.go

@@ -0,0 +1,2 @@
+// Package telegrambot provides a simple entry-to-telegram push
+package telegrambot

+ 41 - 0
integration/telegrambot/telegrambot.go

@@ -0,0 +1,41 @@
+package telegrambot
+
+import (
+	"bytes"
+	"fmt"
+	"html/template"
+	"strconv"
+
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+	"miniflux.app/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: create bot failed: %w", err)
+	}
+
+	t, err := template.New("message").Parse("{{ .Title }}\n<a href=\"{{ .URL }}\">{{ .URL }}</a>")
+	if err != nil {
+		return fmt.Errorf("telegrambot: parse template failed: %w", err)
+	}
+
+	var result bytes.Buffer
+
+	err = t.Execute(&result, entry)
+	if err != nil {
+		return fmt.Errorf("telegrambot: execute template failed: %w", err)
+	}
+
+	chatId, _ := strconv.ParseInt(chatID, 10, 64)
+	msg := tgbotapi.NewMessage(chatId, result.String())
+	msg.ParseMode = tgbotapi.ModeHTML
+	msg.DisableWebPagePreview = false
+	if _, err := bot.Send(msg); err != nil {
+		return fmt.Errorf("telegrambot: send message failed: %w", err)
+	}
+
+	return nil
+}

+ 3 - 0
locale/translations/de_DE.json

@@ -325,6 +325,9 @@
     "form.integration.nunux_keeper_activate": "Artikel in Nunux Keeper speichern",
     "form.integration.nunux_keeper_endpoint": "Nunux Keeper API-Endpunkt",
     "form.integration.nunux_keeper_api_key": "Nunux Keeper API-Schlüssel",
+    "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.api_key.label.description": "API-Schlüsselbezeichnung",
     "form.submit.loading": "Lade...",
     "form.submit.saving": "Speichern...",

+ 3 - 0
locale/translations/el_EL.json

@@ -325,6 +325,9 @@
     "form.integration.nunux_keeper_activate": "Αποθήκευση άρθρων στο Nunux Keeper",
     "form.integration.nunux_keeper_endpoint": "Τελικό σημείο Nunux Keeper API",
     "form.integration.nunux_keeper_api_key": "Κλειδί API Nunux Keeper",
+    "form.integration.telegram_bot_activate": "Προωθήστε νέα άρθρα στη συνομιλία Telegram",
+    "form.integration.telegram_bot_token": "Διακριτικό bot",
+    "form.integration.telegram_chat_id": "Αναγνωριστικό συνομιλίας",
     "form.api_key.label.description": "Ετικέτα κλειδιού API",
     "form.submit.loading": "Φόρτωση...",
     "form.submit.saving": "Αποθήκευση...",

+ 3 - 0
locale/translations/en_US.json

@@ -325,6 +325,9 @@
     "form.integration.nunux_keeper_activate": "Save articles to Nunux Keeper",
     "form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint",
     "form.integration.nunux_keeper_api_key": "Nunux Keeper API key",
+    "form.integration.telegram_bot_activate": "Push new articles to Telegram chat",
+    "form.integration.telegram_bot_token": "Bot token",
+    "form.integration.telegram_chat_id": "Chat ID",
     "form.api_key.label.description": "API Key Label",
     "form.submit.loading": "Loading...",
     "form.submit.saving": "Saving...",

+ 3 - 0
locale/translations/es_ES.json

@@ -325,6 +325,9 @@
     "form.integration.nunux_keeper_activate": "Guardar artículos a Nunux Keeper",
     "form.integration.nunux_keeper_endpoint": "Extremo de API de Nunux Keeper",
     "form.integration.nunux_keeper_api_key": "Clave de API de Nunux Keeper",
+    "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.api_key.label.description": "Etiqueta de clave API",
     "form.submit.loading": "Cargando...",
     "form.submit.saving": "Guardando...",

+ 3 - 0
locale/translations/fr_FR.json

@@ -325,6 +325,9 @@
     "form.integration.nunux_keeper_activate": "Sauvegarder les articles vers Nunux Keeper",
     "form.integration.nunux_keeper_endpoint": "URL de l'API de Nunux Keeper",
     "form.integration.nunux_keeper_api_key": "Clé d'API de Nunux Keeper",
+    "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.api_key.label.description": "Libellé de la clé d'API",
     "form.submit.loading": "Chargement...",
     "form.submit.saving": "Sauvegarde en cours...",

+ 3 - 0
locale/translations/it_IT.json

@@ -325,6 +325,9 @@
     "form.integration.nunux_keeper_activate": "Salva gli articoli su Nunux Keeper",
     "form.integration.nunux_keeper_endpoint": "Endpoint dell'API di Nunux Keeper",
     "form.integration.nunux_keeper_api_key": "API key dell'account Nunux Keeper",
+    "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.api_key.label.description": "Etichetta chiave API",
     "form.submit.loading": "Caricamento in corso...",
     "form.submit.saving": "Salvataggio in corso...",

+ 3 - 0
locale/translations/ja_JP.json

@@ -325,6 +325,9 @@
     "form.integration.nunux_keeper_activate": "Nunux Keeper に記事を保存する",
     "form.integration.nunux_keeper_endpoint": "Nunux Keeper の API Endpoint",
     "form.integration.nunux_keeper_api_key": "Nunux Keeper の API key",
+    "form.integration.telegram_bot_activate": "新しい記事をTelegramチャットにプッシュする",
+    "form.integration.telegram_bot_token": "ボットトークン",
+    "form.integration.telegram_chat_id": "チャットID",
     "form.api_key.label.description": "APIキーラベル",
     "form.submit.loading": "読み込み中…",
     "form.submit.saving": "保存中…",

+ 3 - 0
locale/translations/nl_NL.json

@@ -325,6 +325,9 @@
     "form.integration.nunux_keeper_activate": "Opslaan naar Nunux Keeper",
     "form.integration.nunux_keeper_endpoint": "Nunux Keeper URL",
     "form.integration.nunux_keeper_api_key": "Nunux Keeper API-sleutel",
+    "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.api_key.label.description": "API-sleutellabel",
     "form.submit.loading": "Laden...",
     "form.submit.saving": "Opslaag...",

+ 3 - 0
locale/translations/pl_PL.json

@@ -327,6 +327,9 @@
     "form.integration.nunux_keeper_activate": "Zapisz artykuly do Nunux Keeper",
     "form.integration.nunux_keeper_endpoint": "Nunux Keeper URL",
     "form.integration.nunux_keeper_api_key": "Nunux Keeper API key",
+    "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.api_key.label.description": "Etykieta klucza API",
     "form.submit.loading": "Ładowanie...",
     "form.submit.saving": "Zapisywanie...",

+ 3 - 0
locale/translations/pt_BR.json

@@ -325,6 +325,9 @@
     "form.integration.nunux_keeper_activate": "Salvar itens no Nunux Keeper",
     "form.integration.nunux_keeper_endpoint": "Endpoint de API do Nunux Keeper",
     "form.integration.nunux_keeper_api_key": "Chave de API do Nunux Keeper",
+    "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.api_key.label.description": "Etiqueta da chave de API",
     "form.submit.loading": "Carregando...",
     "form.submit.saving": "Salvando...",

+ 3 - 0
locale/translations/ru_RU.json

@@ -327,6 +327,9 @@
     "form.integration.nunux_keeper_activate": "Сохранять статьи в Nunux Keeper",
     "form.integration.nunux_keeper_endpoint": "Конечная точка Nunux Keeper API",
     "form.integration.nunux_keeper_api_key": "Nunux Keeper API Key",
+    "form.integration.telegram_bot_activate": "Публикуйте новые статьи в Telegram-чате",
+    "form.integration.telegram_bot_token": "Токен бота",
+    "form.integration.telegram_chat_id": "ID чата",
     "form.api_key.label.description": "Описание API-ключа",
     "form.submit.loading": "Загрузка…",
     "form.submit.saving": "Сохранение…",

+ 3 - 0
locale/translations/tr_TR.json

@@ -325,6 +325,9 @@
     "form.integration.nunux_keeper_activate": "Makaleleri Nunux Keeper'a kaydet",
     "form.integration.nunux_keeper_endpoint": "Nunux Keeper API Uç Noktası",
     "form.integration.nunux_keeper_api_key": "Nunux Keeper API anahtarı",
+    "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.api_key.label.description": "API Anahtar Etiketi",
     "form.submit.loading": "Yükleniyor...",
     "form.submit.saving": "Kaydediliyor...",

+ 4 - 1
locale/translations/zh_CN.json

@@ -323,7 +323,10 @@
     "form.integration.nunux_keeper_activate": "保存文章到 Nunux Keeper",
     "form.integration.nunux_keeper_endpoint": "Nunux Keeper API 端点",
     "form.integration.nunux_keeper_api_key": "Nunux Keeper API 密钥",
-    "form.api_key.label.description": "API 密钥标签",
+    "form.integration.telegram_bot_activate": "将新文章推送到 Telegram",
+    "form.integration.telegram_bot_token": "机器人令牌",
+    "form.integration.telegram_chat_id": "聊天ID",
+    "form.api_key.label.description": "API密钥标签",
     "form.submit.loading": "载入中…",
     "form.submit.saving": "保存中…",
     "time_elapsed.not_yet": "未来",

+ 3 - 0
model/integration.go

@@ -29,4 +29,7 @@ type Integration struct {
 	PocketEnabled        bool
 	PocketAccessToken    string
 	PocketConsumerKey    string
+	TelegramBotEnabled   bool
+	TelegramBotToken     string
+	TelegramBotChatID    string
 }

+ 14 - 0
reader/processor/processor.go

@@ -14,6 +14,8 @@ import (
 	"time"
 	"unicode/utf8"
 
+	"miniflux.app/integration"
+
 	"miniflux.app/config"
 	"miniflux.app/http/client"
 	"miniflux.app/logger"
@@ -80,6 +82,18 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed) {
 		// The sanitizer should always run at the end of the process to make sure unsafe HTML is filtered.
 		entry.Content = sanitizer.Sanitize(entry.URL, entry.Content)
 
+		if entryIsNew {
+			intg, err := store.Integration(feed.UserID)
+			if err != nil {
+				logger.Error("[Processor] Get integrations for user %d failed: %v; the refresh process will go on, but no integrations will run this time.", feed.UserID, err)
+			} else if intg != nil {
+				localEntry := entry
+				go func() {
+					integration.PushEntry(localEntry, intg)
+				}()
+			}
+		}
+
 		updateEntryReadingTime(store, feed, entry, entryIsNew)
 		filteredEntries = append(filteredEntries, entry)
 	}

+ 15 - 3
storage/integration.go

@@ -68,7 +68,10 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			nunux_keeper_api_key,
 			pocket_enabled,
 			pocket_access_token,
-			pocket_consumer_key
+			pocket_consumer_key,
+			telegram_bot_enabled,
+			telegram_bot_token,
+			telegram_bot_chat_id
 		FROM
 			integrations
 		WHERE
@@ -99,6 +102,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.PocketEnabled,
 		&integration.PocketAccessToken,
 		&integration.PocketConsumerKey,
+		&integration.TelegramBotEnabled,
+		&integration.TelegramBotToken,
+		&integration.TelegramBotChatID,
 	)
 	switch {
 	case err == sql.ErrNoRows:
@@ -137,9 +143,12 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			nunux_keeper_api_key=$19,
 			pocket_enabled=$20,
 			pocket_access_token=$21,
-			pocket_consumer_key=$22
+			pocket_consumer_key=$22,
+			telegram_bot_enabled=$23,
+			telegram_bot_token=$24,
+			telegram_bot_chat_id=$25
 		WHERE
-			user_id=$23
+			user_id=$26
 	`
 	_, err := s.db.Exec(
 		query,
@@ -165,6 +174,9 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.PocketEnabled,
 		integration.PocketAccessToken,
 		integration.PocketConsumerKey,
+		integration.TelegramBotEnabled,
+		integration.TelegramBotToken,
+		integration.TelegramBotChatID,
 		integration.UserID,
 	)
 

+ 18 - 0
template/templates/views/integrations.html

@@ -136,6 +136,24 @@
         </div>
     </div>
 
+    <h3>Telegram Bot</h3>
+    <div class="form-section">
+        <label>
+            <input type="checkbox" name="telegram_bot_enabled" value="1" {{ if .form.TelegramBotEnabled }}checked{{ end }}> {{ t "form.integration.telegram_bot_activate" }}
+        </label>
+
+        <label for="form-telegram-bot-token">{{ t "form.integration.telegram_bot_token" }}</label>
+        <input type="text" name="telegram_bot_token" id="form-telegram-bot-token" value="{{ .form.TelegramBotToken }}" placeholder="bot123456:Abcdefg" spellcheck="false">
+
+        <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">
+
+        <div class="buttons">
+            <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
+        </div>
+    </div>
+
+
 </form>
 
 <h3>{{ t "page.integration.bookmarklet" }}</h3>

+ 9 - 0
ui/form/integration.go

@@ -34,6 +34,9 @@ type IntegrationForm struct {
 	PocketEnabled        bool
 	PocketAccessToken    string
 	PocketConsumerKey    string
+	TelegramBotEnabled   bool
+	TelegramBotToken     string
+	TelegramBotChatID    string
 }
 
 // Merge copy form values to the model.
@@ -59,6 +62,9 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.PocketEnabled = i.PocketEnabled
 	integration.PocketAccessToken = i.PocketAccessToken
 	integration.PocketConsumerKey = i.PocketConsumerKey
+	integration.TelegramBotEnabled = i.TelegramBotEnabled
+	integration.TelegramBotToken = i.TelegramBotToken
+	integration.TelegramBotChatID = i.TelegramBotChatID
 }
 
 // NewIntegrationForm returns a new AuthForm.
@@ -86,5 +92,8 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
 		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"),
 	}
 }

+ 3 - 0
ui/integration_show.go

@@ -50,6 +50,9 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
 		PocketEnabled:        integration.PocketEnabled,
 		PocketAccessToken:    integration.PocketAccessToken,
 		PocketConsumerKey:    integration.PocketConsumerKey,
+		TelegramBotEnabled:   integration.TelegramBotEnabled,
+		TelegramBotToken:     integration.TelegramBotToken,
+		TelegramBotChatID:    integration.TelegramBotChatID,
 	}
 
 	sess := session.New(h.store, request.SessionID(r))