1
mirror of https://github.com/jlelse/GoBlog synced 2024-05-30 11:54:28 +00:00

Rework Telegram implementation

This commit is contained in:
Jan-Lukas Else 2024-05-13 09:21:28 +02:00
parent 40d4a7952e
commit d60e613bbd
6 changed files with 96 additions and 154 deletions

View File

@ -294,10 +294,9 @@ type configNtfy struct {
}
type configTelegram struct {
Enabled bool `mapstructure:"enabled"`
ChatID string `mapstructure:"chatId"`
BotToken string `mapstructure:"botToken"`
InstantViewHash string `mapstructure:"instantViewHash"`
Enabled bool `mapstructure:"enabled"`
ChatID string `mapstructure:"chatId"`
BotToken string `mapstructure:"botToken"`
}
type configMatrix struct {

View File

@ -255,7 +255,6 @@ blogs:
enabled: true # Enable
chatId: "@telegram" # Chat ID, usually channel username
botToken: BOT-TOKEN # Telegram Bot Token
instantViewHash: INSTANT-VIEW-HASH # Use custom TG IV template
# Comments
comments:
enabled: true # Enable comments

3
go.mod
View File

@ -26,7 +26,6 @@ require (
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/go-chi/chi/v5 v5.0.12
github.com/go-fed/httpsig v1.1.0
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/gorilla/sessions v1.2.2
@ -52,7 +51,7 @@ require (
github.com/spf13/cast v1.6.0
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.9.0
github.com/tdewolff/minify/v2 v2.20.20
github.com/tdewolff/minify/v2 v2.20.21
github.com/tiptophelmet/cspolicy v0.1.1
github.com/tkrajina/gpxgo v1.4.0
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80

6
go.sum
View File

@ -85,8 +85,6 @@ github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -253,8 +251,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tdewolff/minify/v2 v2.20.20 h1:vhULb+VsW2twkplgsawAoUY957efb+EdiZ7zu5fUhhk=
github.com/tdewolff/minify/v2 v2.20.20/go.mod h1:GYaLXFpIIwsX99apQHXfGdISUdlA98wmaoWxjT9C37k=
github.com/tdewolff/minify/v2 v2.20.21 h1:8MCHcxXAVO8B7X+v07mwMWBIEtQo65e1JzBqDgZOQpU=
github.com/tdewolff/minify/v2 v2.20.21/go.mod h1:GYaLXFpIIwsX99apQHXfGdISUdlA98wmaoWxjT9C37k=
github.com/tdewolff/parse/v2 v2.7.14 h1:100KJ+QAO3PpMb3uUjzEU/NpmCdbBYz6KPmCIAfWpR8=
github.com/tdewolff/parse/v2 v2.7.14/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=

View File

@ -1,11 +1,12 @@
package main
import (
"errors"
"net/url"
"context"
"fmt"
"strconv"
"strings"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/carlmjohnson/requests"
"go.goblog.app/app/pkgs/builderpool"
)
@ -23,21 +24,26 @@ func (tg *configTelegram) enabled() bool {
return true
}
const (
telegramChatParam = "telegramchat"
telegramMsgParam = "telegrammsg"
)
func (a *goBlog) tgPost(p *post, silent bool) {
if tg := a.getBlogFromPost(p).Telegram; tg.enabled() && p.isPublicPublishedSectionPost() {
tgChat := p.firstParameter("telegramchat")
tgMsg := p.firstParameter("telegrammsg")
tgChat := p.firstParameter(telegramChatParam)
tgMsg := p.firstParameter(telegramMsgParam)
if tgChat != "" && tgMsg != "" {
// Already posted
return
}
// Generate HTML
html := tg.generateHTML(p.RenderedTitle, a.fullPostURL(p), a.shortPostURL(p))
html := tg.generateHTML(p.RenderedTitle, a.shortPostURL(p))
if html == "" {
return
}
// Send message
chatId, msgId, err := a.sendTelegram(tg, html, tgbotapi.ModeHTML, silent)
chatId, msgId, err := a.sendTelegram(tg, html, "HTML", silent)
if err != nil {
a.error("Failed to send post to Telegram", "err", err)
return
@ -47,12 +53,10 @@ func (a *goBlog) tgPost(p *post, silent bool) {
return
}
// Save chat and message id to post
err = a.db.replacePostParam(p.Path, "telegramchat", []string{strconv.FormatInt(chatId, 10)})
if err != nil {
if err := a.db.replacePostParam(p.Path, telegramChatParam, []string{strconv.FormatInt(chatId, 10)}); err != nil {
a.error("Failed to save Telegram chat id", "err", err)
}
err = a.db.replacePostParam(p.Path, "telegrammsg", []string{strconv.Itoa(msgId)})
if err != nil {
if err := a.db.replacePostParam(p.Path, telegramMsgParam, []string{strconv.Itoa(msgId)}); err != nil {
a.error("Failed to save Telegram message id", "err", err)
}
}
@ -60,32 +64,19 @@ func (a *goBlog) tgPost(p *post, silent bool) {
func (a *goBlog) tgUpdate(p *post) {
if tg := a.getBlogFromPost(p).Telegram; tg.enabled() {
tgChat := p.firstParameter("telegramchat")
tgMsg := p.firstParameter("telegrammsg")
tgChat := p.firstParameter(telegramChatParam)
tgMsg := p.firstParameter(telegramMsgParam)
if tgChat == "" || tgMsg == "" {
// Not send to Telegram
return
}
// Parse tgChat to int64
chatId, err := strconv.ParseInt(tgChat, 10, 64)
if err != nil {
a.error("Failed to parse Telegram chat ID", "err", err)
return
}
// Parse tgMsg to int
messageId, err := strconv.Atoi(tgMsg)
if err != nil {
a.error("Failed to parse Telegram message ID", "err", err)
return
}
// Generate HTML
html := tg.generateHTML(p.RenderedTitle, a.fullPostURL(p), a.shortPostURL(p))
html := tg.generateHTML(p.RenderedTitle, a.shortPostURL(p))
if html == "" {
return
}
// Send update
err = a.updateTelegram(tg, chatId, messageId, html, "HTML")
if err != nil {
if err := a.updateTelegram(tg, tgChat, tgMsg, html, "HTML"); err != nil {
a.error("Failed to send update to Telegram", "err", err)
}
}
@ -93,143 +84,120 @@ func (a *goBlog) tgUpdate(p *post) {
func (a *goBlog) tgDelete(p *post) {
if tg := a.getBlogFromPost(p).Telegram; tg.enabled() {
tgChat := p.firstParameter("telegramchat")
tgMsg := p.firstParameter("telegrammsg")
tgChat := p.firstParameter(telegramChatParam)
tgMsg := p.firstParameter(telegramMsgParam)
if tgChat == "" || tgMsg == "" {
// Not send to Telegram
return
}
// Parse tgChat to int64
chatId, err := strconv.ParseInt(tgChat, 10, 64)
if err != nil {
a.error("Failed to parse Telegram chat ID", "err", err)
return
}
// Parse tgMsg to int
messageId, err := strconv.Atoi(tgMsg)
if err != nil {
a.error("Failed to parse Telegram message ID", "err", err)
return
}
// Delete message
err = a.deleteTelegram(tg, chatId, messageId)
if err != nil {
if err := a.deleteTelegram(tg, tgChat, tgMsg); err != nil {
a.error("Failed to delete Telegram message", "err", err)
}
// Delete chat and message id from post
err = a.db.replacePostParam(p.Path, "telegramchat", []string{})
if err != nil {
if err := a.db.replacePostParam(p.Path, telegramChatParam, []string{}); err != nil {
a.error("Failed to remove Telegram chat id", "err", err)
}
err = a.db.replacePostParam(p.Path, "telegrammsg", []string{})
if err != nil {
if err := a.db.replacePostParam(p.Path, telegramMsgParam, []string{}); err != nil {
a.error("Failed to remove Telegram message id", "err", err)
}
}
}
func (tg *configTelegram) generateHTML(title, fullURL, shortURL string) (html string) {
func (tg *configTelegram) generateHTML(title, shortURL string) string {
if !tg.enabled() {
return ""
}
message := builderpool.Get()
defer builderpool.Put(message)
tgReplacer := strings.NewReplacer("<", "&lt;", ">", "&gt;", "&", "&amp;")
if title != "" {
message.WriteString(tgbotapi.EscapeText(tgbotapi.ModeHTML, title))
tgReplacer.WriteString(message, title)
message.WriteString("\n\n")
}
if tg.InstantViewHash != "" {
message.WriteString("<a href=\"https://t.me/iv?rhash=" + tg.InstantViewHash + "&url=" + url.QueryEscape(fullURL) + "\">")
message.WriteString(tgbotapi.EscapeText(tgbotapi.ModeHTML, shortURL))
message.WriteString("</a>")
} else {
message.WriteString("<a href=\"" + shortURL + "\">")
message.WriteString(tgbotapi.EscapeText(tgbotapi.ModeHTML, shortURL))
message.WriteString("</a>")
}
html = message.String()
return
message.WriteString("<a href=\"" + shortURL + "\">")
tgReplacer.WriteString(message, shortURL)
message.WriteString("</a>")
return message.String()
}
type telegramMessageResult struct {
OK bool `json:"ok"`
Description string `json:"description"`
Result struct {
Chat struct {
ID int64 `json:"id"`
} `json:"chat"`
MessageID int `json:"message_id"`
} `json:"result"`
}
func (a *goBlog) sendTelegram(tg *configTelegram, message, mode string, silent bool) (int64, int, error) {
if !tg.enabled() {
return 0, 0, nil
}
bot, err := tgbotapi.NewBotAPIWithClient(tg.BotToken, tgbotapi.APIEndpoint, a.httpClient)
if err != nil {
telegramURL := "https://api.telegram.org/bot" + tg.BotToken + "/sendMessage"
result := &telegramMessageResult{}
if err := requests.URL(telegramURL).Client(a.httpClient).
Param("chat_id", tg.ChatID).
Param("text", message).
Param("parse_mode", mode).
Param("disable_notification", strconv.FormatBool(silent)).
ToJSON(result).
Fetch(context.Background()); err != nil {
return 0, 0, err
}
msg := tgbotapi.MessageConfig{
BaseChat: tgbotapi.BaseChat{
ChannelUsername: tg.ChatID,
DisableNotification: silent,
},
Text: message,
ParseMode: mode,
if !result.OK {
return 0, 0, fmt.Errorf("error from Telegram API: %s", result.Description)
}
res, err := bot.Send(msg)
if err != nil {
return 0, 0, err
}
return res.Chat.ID, res.MessageID, nil
return result.Result.Chat.ID, result.Result.MessageID, nil
}
func (a *goBlog) updateTelegram(tg *configTelegram, chatId int64, messageId int, message, mode string) error {
func (a *goBlog) updateTelegram(tg *configTelegram, chatID, messageID, message, mode string) error {
if !tg.enabled() {
return nil
}
bot, err := tgbotapi.NewBotAPIWithClient(tg.BotToken, tgbotapi.APIEndpoint, a.httpClient)
if err != nil {
telegramURL := "https://api.telegram.org/bot" + tg.BotToken + "/editMessageText"
result := &telegramMessageResult{}
if err := requests.URL(telegramURL).Client(a.httpClient).
Param("chat_id", chatID).
Param("message_id", messageID).
Param("text", message).
Param("parse_mode", mode).
ToJSON(result).
Fetch(context.Background()); err != nil {
return err
}
// Check if chat is still the configured one
chat, err := bot.GetChat(tgbotapi.ChatInfoConfig{
ChatConfig: tgbotapi.ChatConfig{
SuperGroupUsername: tg.ChatID,
},
})
if err != nil {
return err
if !result.OK {
return fmt.Errorf("error from Telegram API: %s", result.Description)
}
if chat.ID != chatId {
return errors.New("chat id mismatch")
}
// Send update
msg := tgbotapi.EditMessageTextConfig{
BaseEdit: tgbotapi.BaseEdit{
ChatID: chatId,
MessageID: messageId,
},
Text: message,
ParseMode: mode,
}
_, err = bot.Send(msg)
return err
return nil
}
func (a *goBlog) deleteTelegram(tg *configTelegram, chatId int64, messageId int) error {
func (a *goBlog) deleteTelegram(tg *configTelegram, chatID, messageID string) error {
if !tg.enabled() {
return nil
}
bot, err := tgbotapi.NewBotAPIWithClient(tg.BotToken, tgbotapi.APIEndpoint, a.httpClient)
if err != nil {
telegramURL := "https://api.telegram.org/bot" + tg.BotToken + "/deleteMessage"
result := &telegramMessageResult{}
if err := requests.URL(telegramURL).Client(a.httpClient).
Param("chat_id", chatID).
Param("message_id", messageID).
ToJSON(result).
Fetch(context.Background()); err != nil {
return err
}
chat, err := bot.GetChat(tgbotapi.ChatInfoConfig{
ChatConfig: tgbotapi.ChatConfig{
SuperGroupUsername: tg.ChatID,
},
})
if err != nil {
return err
if !result.OK {
return fmt.Errorf("error from Telegram API: %s", result.Description)
}
if chat.ID != chatId {
return errors.New("chat id mismatch")
}
msg := tgbotapi.DeleteMessageConfig{
ChatID: chatId,
MessageID: messageId,
}
_, err = bot.Send(msg)
return err
return nil
}

View File

@ -28,18 +28,8 @@ func Test_configTelegram_generateHTML(t *testing.T) {
BotToken: "abc",
}
// Without Instant View
expected := "Title\n\n<a href=\"https://example.com/s/1\">https://example.com/s/1</a>"
if got := tg.generateHTML("Title", "https://example.com/test", "https://example.com/s/1"); got != expected {
t.Errorf("Wrong result, got: %v", got)
}
// With Instant View
tg.InstantViewHash = "abc"
expected = "Title\n\n<a href=\"https://t.me/iv?rhash=abc&url=https%3A%2F%2Fexample.com%2Ftest\">https://example.com/s/1</a>"
if got := tg.generateHTML("Title", "https://example.com/test", "https://example.com/s/1"); got != expected {
if got := tg.generateHTML("Title", "https://example.com/s/1"); got != expected {
t.Errorf("Wrong result, got: %v", got)
}
}
@ -48,11 +38,6 @@ func Test_configTelegram_send(t *testing.T) {
fakeClient := newFakeHttpClient()
fakeClient.setHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.String() == "https://api.telegram.org/botbottoken/getMe" {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(`{"ok":true,"result":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"}}`))
return
}
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(`{"ok":true,"result":{"message_id":123,"from":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"},"chat":{"id":789,"first_name":"Test","username":"testbot"},"date":1564181818,"text":"Message"}}`))
}))
@ -74,8 +59,7 @@ func Test_configTelegram_send(t *testing.T) {
assert.Equal(t, int64(789), chatId)
assert.NotNil(t, fakeClient.req)
assert.Equal(t, http.MethodPost, fakeClient.req.Method)
assert.Equal(t, "https://api.telegram.org/botbottoken/sendMessage", fakeClient.req.URL.String())
assert.Contains(t, fakeClient.req.URL.String(), "https://api.telegram.org/botbottoken/sendMessage")
req := fakeClient.req
assert.Equal(t, "chatid", req.FormValue("chat_id"))
@ -99,11 +83,6 @@ func Test_telegram(t *testing.T) {
t.Run("Send post to Telegram", func(t *testing.T) {
fakeClient := newFakeHttpClient()
fakeClient.setHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.String() == "https://api.telegram.org/botbottoken/getMe" {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(`{"ok":true,"result":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"}}`))
return
}
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(`{"ok":true,"result":{"message_id":123,"from":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"},"chat":{"id":123456789,"first_name":"Test","username":"testbot"},"date":1564181818,"text":"Message"}}`))
}))
@ -139,7 +118,7 @@ func Test_telegram(t *testing.T) {
app.pPostHooks[0](p)
assert.Equal(t, "https://api.telegram.org/botbottoken/sendMessage", fakeClient.req.URL.String())
assert.Contains(t, fakeClient.req.URL.String(), "https://api.telegram.org/botbottoken/sendMessage")
req := fakeClient.req
assert.Equal(t, "chatid", req.FormValue("chat_id"))