penguin/golifehk

@golifehk bot in Telegram

commit f149a074980be5933f95cef4de286d42831a2c7d

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-06-20T02:14:09Z
subjectAttach message as text file if message too long
commit f149a074980be5933f95cef4de286d42831a2c7d
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-06-20T02:14:09Z

    Attach message as text file if message too long
---
 adaptors/tg/botsend.go                    | 107 ++++++++++++++++++++++++------
 cmd/README.md                             |   4 ++
 datasources/cjlookup/QueryResult.go       |   1 +
 datasources/kmb/QueryResult.go            |   1 +
 datasources/mtr/bus/QueryResult.go        |   1 +
 i18n/messages.go                          |   1 +
 main.go                                   |  10 +--
 query/objects.go                          |   1 +
 resources/langpacks/zh-Hant/messages.json |   1 +
 resources/po/zh-Hant/messages.po          |   8 ++-
 resources/pot/messages.pot                |   8 ++-
 resources/src/messages.json               |   1 +
 utils/shortcuts.go                        |  11 +++
 13 files changed, 126 insertions(+), 29 deletions(-)

diff --git a/adaptors/tg/botsend.go b/adaptors/tg/botsend.go
index 8625dc4..57a635a 100644
--- a/adaptors/tg/botsend.go
+++ b/adaptors/tg/botsend.go
@@ -1,26 +1,33 @@
 package tg
 
 import (
+	"fmt"
+	"strings"
+
 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
+	i18n "github.com/tgckpg/golifehk/i18n"
 	query "github.com/tgckpg/golifehk/query"
+	utils "github.com/tgckpg/golifehk/utils"
 )
 
-func BotSendText(bot *tgbotapi.BotAPI, update *tgbotapi.Update, mesg *string) {
-	var msg tgbotapi.MessageConfig
-	msg = tgbotapi.NewMessage(update.Message.Chat.ID, *mesg)
+func BotSendText(lang string, bot *tgbotapi.BotAPI, update *tgbotapi.Update, mesg *string) error {
+	if update == nil || update.Message == nil || mesg == nil {
+		return fmt.Errorf("nil update/message/text")
+	}
+
+	chatID := update.Message.Chat.ID
+	replyTo := update.Message.MessageID
+
+	msg := tgbotapi.NewMessage(chatID, *mesg)
 	msg.ParseMode = "MarkdownV2"
+	msg.ReplyToMessageID = replyTo
 
-	msg.ReplyToMessageID = update.Message.MessageID
-	bot.Send(msg)
+	return sendMessageOrFile(lang, bot, msg, *mesg)
 }
 
 func BotSend(bot *tgbotapi.BotAPI, update *tgbotapi.Update, qResult query.IQueryResult) (bool, error) {
-
-	var msg tgbotapi.MessageConfig
 	mesg, err := qResult.Message()
-
 	if err != nil {
-		// not sent, error
 		return false, err
 	}
 
@@ -28,40 +35,98 @@ func BotSend(bot *tgbotapi.BotAPI, update *tgbotapi.Update, qResult query.IQuery
 
 	switch mesgType {
 	case "IGNORE":
-		// not sent with no error tells the parent to look for other processors
 		return false, nil
 	}
 
-	var chatId int64
-	if update.Message != nil {
-		chatId = update.Message.Chat.ID
-		msg.ReplyToMessageID = update.Message.MessageID
-	}
-
-	if update.CallbackQuery != nil {
-		chatId = update.CallbackQuery.Message.Chat.ID
+	chatID, replyTo, err := getChatAndReply(update)
+	if err != nil {
+		return false, err
 	}
 
-	msg = tgbotapi.NewMessage(chatId, mesg)
+	msg := tgbotapi.NewMessage(chatID, mesg)
 	msg.ParseMode = "MarkdownV2"
 
+	if replyTo != 0 {
+		msg.ReplyToMessageID = replyTo
+	}
+
 	switch mesgType {
 	case "PlainText":
-	case "Table":
 
+	case "Table":
 		buttonRows := [][]tgbotapi.InlineKeyboardButton{}
+
 		for _, row := range qResult.GetTableData() {
 			buttons := []tgbotapi.InlineKeyboardButton{}
+
 			for _, cell := range row {
 				button := tgbotapi.NewInlineKeyboardButtonData(cell.Name, cell.Value)
 				buttons = append(buttons, button)
 			}
+
 			buttonRows = append(buttonRows, buttons)
 		}
 
 		msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(buttonRows...)
 	}
 
-	bot.Send(msg)
+	if err := sendMessageOrFile(qResult.UserLang(), bot, msg, mesg); err != nil {
+		return false, err
+	}
+
 	return true, nil
 }
+
+func getChatAndReply(update *tgbotapi.Update) (chatID int64, replyTo int, err error) {
+	if update == nil {
+		return 0, 0, fmt.Errorf("nil update")
+	}
+
+	if update.Message != nil {
+		return update.Message.Chat.ID, update.Message.MessageID, nil
+	}
+
+	if update.CallbackQuery != nil && update.CallbackQuery.Message != nil {
+		return update.CallbackQuery.Message.Chat.ID, 0, nil
+	}
+
+	return 0, 0, fmt.Errorf("update has no message or callback message")
+}
+
+func sendMessageOrFile(lang string, bot *tgbotapi.BotAPI, msg tgbotapi.MessageConfig, text string) error {
+	_, err := bot.Send(msg)
+	if err == nil {
+		return nil
+	}
+
+	if !strings.Contains(err.Error(), "message is too long") {
+		return err
+	}
+
+	doc := tgbotapi.NewDocument(
+		msg.ChatID,
+		tgbotapi.FileBytes{
+			Name:  "message.txt",
+			Bytes: []byte(utils.UnescapeMDv2Text(text)),
+		},
+	)
+
+	langPack, err := i18n.LoadKeys(lang)
+	doc.Caption = i18n.TG_MESG_TOO_LONG.Text(langPack)
+
+	if msg.ReplyToMessageID != 0 {
+		doc.ReplyToMessageID = msg.ReplyToMessageID
+	}
+
+	// Keep inline keyboard if this was a table result.
+	//
+	// This is useful when the text is too long but the buttons are still needed.
+	doc.ReplyMarkup = msg.ReplyMarkup
+
+	_, docErr := bot.Send(doc)
+	if docErr != nil {
+		return fmt.Errorf("send message failed: %w; send document failed: %w", err, docErr)
+	}
+
+	return nil
+}
diff --git a/cmd/README.md b/cmd/README.md
new file mode 100644
index 0000000..d0df092
--- /dev/null
+++ b/cmd/README.md
@@ -0,0 +1,4 @@
+1. Edit i18n/messages.go
+2. Run cmd/po-export.sh
+3. Update resources/pot/messages.pot and messages.po
+4. Run cmd/po-build.sh
diff --git a/datasources/cjlookup/QueryResult.go b/datasources/cjlookup/QueryResult.go
index ffc7425..572000c 100644
--- a/datasources/cjlookup/QueryResult.go
+++ b/datasources/cjlookup/QueryResult.go
@@ -41,6 +41,7 @@ func writeCCharInfo(sb *strings.Builder, cc *CChar) {
 }
 
 func (this QueryResult) DataType() string                  { return this.ResultType }
+func (this QueryResult) UserLang() string                  { return this.Lang }
 func (this QueryResult) Consumed() bool                    { return this.isConsumed }
 func (this QueryResult) GetTableData() [][]query.TableCell { return nil }
 
diff --git a/datasources/kmb/QueryResult.go b/datasources/kmb/QueryResult.go
index 486dd2e..fa77e43 100644
--- a/datasources/kmb/QueryResult.go
+++ b/datasources/kmb/QueryResult.go
@@ -59,6 +59,7 @@ func writeShortRoute(lang *string, sb *strings.Builder, r *RouteStop) {
 
 func (this QueryResult) DataType() string                  { return this.dataType }
 func (this QueryResult) Consumed() bool                    { return this.isConsumed }
+func (this QueryResult) UserLang() string                  { return this.Lang }
 func (this QueryResult) GetTableData() [][]query.TableCell { return this.tableData }
 
 func (this *QueryResult) Message() (string, error) {
diff --git a/datasources/mtr/bus/QueryResult.go b/datasources/mtr/bus/QueryResult.go
index 0ae96fc..9d8789d 100644
--- a/datasources/mtr/bus/QueryResult.go
+++ b/datasources/mtr/bus/QueryResult.go
@@ -45,6 +45,7 @@ func writeShortRoute(lang *string, sb *strings.Builder, b *BusStop) {
 }
 
 func (this QueryResult) DataType() string                  { return this.dataType }
+func (this QueryResult) UserLang() string                  { return this.Lang }
 func (this QueryResult) Consumed() bool                    { return this.isConsumed }
 func (this QueryResult) GetTableData() [][]query.TableCell { return this.tableData }
 
diff --git a/i18n/messages.go b/i18n/messages.go
index 3f42809..8da4a3d 100644
--- a/i18n/messages.go
+++ b/i18n/messages.go
@@ -9,6 +9,7 @@ const (
 	DS_MTR_NO_NEAREST_STOPS Key = "DS.MTR.NO_NEAREST_STOPS"
 	DS_MTR_NO_SCHEDULES     Key = "DS.MTR.NO_SCHEDULES"
 	NO_RESULTS              Key = "NO_RESULTS"
+	TG_MESG_TOO_LONG        Key = "TG.MESSAGE_TOO_LONG"
 	UNITS_MINUTE            Key = "UNITS.MINUTE"
 	UNITS_KM                Key = "UNITS.KM"
 	UNITS_METER             Key = "UNITS.METER"
diff --git a/main.go b/main.go
index f32b85f..635a13b 100644
--- a/main.go
+++ b/main.go
@@ -31,6 +31,8 @@ func main() {
 
 	for update := range updates {
 
+		lang := "zh-Hant"
+
 		if update.CallbackQuery != nil {
 			callback := tgbotapi.NewCallback(update.CallbackQuery.ID, "")
 			bot.Request(callback)
@@ -40,7 +42,7 @@ func main() {
 
 			if !f_sent && f_err != nil {
 				mesg := utils.MDv2Text(fmt.Sprintf("%s", f_err))
-				tgadaptor.BotSendText(bot, &update, &mesg)
+				tgadaptor.BotSendText(lang, bot, &update, &mesg)
 			}
 
 			continue
@@ -56,13 +58,13 @@ func main() {
 		mesg, processed := utils.SystemControl(update.Message)
 		if processed {
 			if mesg != "" {
-				tgadaptor.BotSendText(bot, &update, &mesg)
+				tgadaptor.BotSendText(lang, bot, &update, &mesg)
 			}
 			continue
 		}
 
 		tgMesg := update.Message
-		q := query.QueryMessage{Lang: "zh-Hant", Text: tgMesg.Text}
+		q := query.QueryMessage{Lang: lang, Text: tgMesg.Text}
 
 		if tgMesg.Location != nil {
 			q.Location = &query.GeoLocation{tgMesg.Location.Latitude, tgMesg.Location.Longitude}
@@ -72,7 +74,7 @@ func main() {
 
 		if !isGroup && !f_sent && f_err != nil {
 			mesg := utils.MDv2Text(fmt.Sprintf("%s", f_err))
-			tgadaptor.BotSendText(bot, &update, &mesg)
+			tgadaptor.BotSendText(lang, bot, &update, &mesg)
 		}
 	}
 }
diff --git a/query/objects.go b/query/objects.go
index 50120e0..d56d332 100644
--- a/query/objects.go
+++ b/query/objects.go
@@ -29,4 +29,5 @@ type IQueryResult interface {
 	DataType() string
 	GetTableData() [][]TableCell
 	Consumed() bool
+	UserLang() string
 }
diff --git a/resources/langpacks/zh-Hant/messages.json b/resources/langpacks/zh-Hant/messages.json
index aacfa2a..29d53ab 100644
--- a/resources/langpacks/zh-Hant/messages.json
+++ b/resources/langpacks/zh-Hant/messages.json
@@ -7,6 +7,7 @@
     "DS.MTR.NO_NEAREST_STOPS": "%s 範圍內找不到港鐵巴士站,最近 %d 個站為:",
     "DS.MTR.NO_SCHEDULES": "沒有行程(收左工了?全日得一班?)",
     "NO_RESULTS": "找不到結果",
+    "TG.MESSAGE_TOO_LONG": "訊息太長,以附件方式傳送",
     "UNITS.KM": "%.1f 公里",
     "UNITS.METER": "%.0f 米",
     "UNITS.MILE": "%.1f 英哩",
diff --git a/resources/po/zh-Hant/messages.po b/resources/po/zh-Hant/messages.po
index 743b3a8..5143b11 100644
--- a/resources/po/zh-Hant/messages.po
+++ b/resources/po/zh-Hant/messages.po
@@ -3,14 +3,14 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-10 16:50+0800\n"
+"POT-Creation-Date: 2026-06-20 02:13+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Translate Toolkit 3.19.3\n"
+"X-Generator: Translate Toolkit 3.19.11\n"
 
 #: .DS.KMB.ETA.DEPARTED
 msgid "%s (Leaved?)"
@@ -49,6 +49,10 @@ msgstr "沒有行程(收左工了?全日得一班?)"
 msgid "No Results"
 msgstr "找不到結果"
 
+#: .TG.MESSAGE_TOO_LONG
+msgid "Message was too long, attached as text file."
+msgstr "訊息太長,以附件方式傳送"
+
 #: .UNITS.KM
 msgid "%.1f km"
 msgstr "%.1f 公里"
diff --git a/resources/pot/messages.pot b/resources/pot/messages.pot
index 8acf082..84c4c11 100644
--- a/resources/pot/messages.pot
+++ b/resources/pot/messages.pot
@@ -4,14 +4,14 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-03-10 16:50+0800\n"
+"POT-Creation-Date: 2026-06-20 02:13+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Translate Toolkit 3.19.3\n"
+"X-Generator: Translate Toolkit 3.19.11\n"
 
 #: .DS.KMB.ETA.DEPARTED
 msgid "%s (Leaved?)"
@@ -50,6 +50,10 @@ msgstr ""
 msgid "No Results"
 msgstr ""
 
+#: .TG.MESSAGE_TOO_LONG
+msgid "Message was too long, attached as text file."
+msgstr ""
+
 #: .UNITS.KM
 msgid "%.1f km"
 msgstr ""
diff --git a/resources/src/messages.json b/resources/src/messages.json
index 2b80ed1..81641a7 100644
--- a/resources/src/messages.json
+++ b/resources/src/messages.json
@@ -7,6 +7,7 @@
   "DS.MTR.NO_NEAREST_STOPS": "MTR Bus: Unable to find stations under %s. Listing nearest %d stations instead:",
   "DS.MTR.NO_SCHEDULES": "Schedules are empty...perhaps Out of Service Time?",
   "NO_RESULTS": "No Results",
+  "TG.MESSAGE_TOO_LONG": "Message was too long, attached as text file.",
   "UNITS.KM": "%.1f km",
   "UNITS.METER": "%.0f m",
   "UNITS.MILE": "%.1f mi",
diff --git a/utils/shortcuts.go b/utils/shortcuts.go
index 7aa2ab1..3451b20 100644
--- a/utils/shortcuts.go
+++ b/utils/shortcuts.go
@@ -40,6 +40,17 @@ func MDv2Text(t string) string {
 	return sb.String()
 }
 
+func UnescapeMDv2Text(t string) string {
+	sb := strings.Builder{}
+
+	for _, c := range MARKDOWN_ESC {
+		t = strings.Replace(t, "\\"+c, c, -1)
+	}
+
+	sb.WriteString(t)
+	return sb.String()
+}
+
 func IsASCIIAlnum(s string) bool {
 	if s == "" {
 		return false