Files
mygoTgChanBot/internal/toc/manager.go
zuimao_kaf df1fb034b3 TOC分页:解决内容超长导致MEDIA_CAPTION_TOO_LONG错误
当目录条目增多超过Telegram caption字符限制时,自动按字符上限分页渲染,
附带翻页inline keyboard(⏮首页/⬅️上一页/➡️下一页),支持photo和text两种模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 22:15:54 +08:00

243 lines
5.1 KiB
Go

package toc
import (
"fmt"
"log"
"strings"
"sync"
"time"
"tgchanbot/internal/storage"
tele "gopkg.in/telebot.v3"
)
const (
captionCharLimit = 950 // photo caption 字符上限(预留安全余量)
textCharLimit = 3900 // 纯文本消息字符上限
// CbPrefixTocPage 翻页回调前缀,导出供 telegram 包使用
CbPrefixTocPage = "tocpage:"
)
type Manager struct {
storage *storage.Storage
bot *tele.Bot
chanID int64
debounce time.Duration
coverImage string
mu sync.Mutex
pending bool
timer *time.Timer
stopCh chan struct{}
}
func NewManager(store *storage.Storage, bot *tele.Bot, chanID int64, debounce time.Duration, coverImage string) *Manager {
return &Manager{
storage: store,
bot: bot,
chanID: chanID,
debounce: debounce,
coverImage: coverImage,
stopCh: make(chan struct{}),
}
}
func (m *Manager) TriggerUpdate() {
m.mu.Lock()
defer m.mu.Unlock()
m.pending = true
if m.timer != nil {
m.timer.Stop()
}
m.timer = time.AfterFunc(m.debounce, func() {
m.doUpdate()
})
}
func (m *Manager) doUpdate() {
m.mu.Lock()
m.pending = false
m.mu.Unlock()
if err := m.DoUpdatePage(1); err != nil {
log.Printf("TOC update error: %v", err)
}
}
// DoUpdatePage 渲染指定页并更新频道 TOC 消息
func (m *Manager) DoUpdatePage(page int) error {
charLimit := captionCharLimit
if m.coverImage == "" {
charLimit = textCharLimit
}
content, totalPages, err := m.RenderPage(page, charLimit)
if err != nil {
return fmt.Errorf("TOC render error: %w", err)
}
var markup *tele.ReplyMarkup
if totalPages > 1 {
markup = m.buildPageKeyboard(page, totalPages)
}
return m.updateMessage(content, markup)
}
// HandlePageCallback 处理翻页回调,供 telegram 包调用
func (m *Manager) HandlePageCallback(page int) error {
return m.DoUpdatePage(page)
}
// buildPageKeyboard 构建翻页 inline keyboard
func (m *Manager) buildPageKeyboard(currentPage, totalPages int) *tele.ReplyMarkup {
menu := &tele.ReplyMarkup{}
var btns []tele.Btn
// 回到第一页(非首页时显示)
if currentPage > 1 {
btns = append(btns, menu.Data("⏮", CbPrefixTocPage+"1"))
}
// 上一页(非首页时显示)
if currentPage > 1 {
btns = append(btns, menu.Data("⬅️", fmt.Sprintf("%s%d", CbPrefixTocPage, currentPage-1)))
}
// 下一页(非末页时显示)
if currentPage < totalPages {
btns = append(btns, menu.Data("➡️", fmt.Sprintf("%s%d", CbPrefixTocPage, currentPage+1)))
}
if len(btns) > 0 {
menu.Inline(menu.Row(btns...))
}
return menu
}
func (m *Manager) updateMessage(content string, markup *tele.ReplyMarkup) error {
chat := &tele.Chat{ID: m.chanID}
msgID, err := m.storage.GetTocMsgID()
if err != nil {
return err
}
// 带封面图片模式
if m.coverImage != "" {
return m.updateWithPhoto(chat, msgID, content, markup)
}
// 纯文本模式
if msgID == 0 {
return m.sendNewText(chat, content, markup)
}
existingMsg := &tele.Message{
ID: msgID,
Chat: chat,
}
opts := []interface{}{tele.ModeHTML, tele.NoPreview}
if markup != nil {
opts = append(opts, markup)
}
_, err = m.bot.Edit(existingMsg, content, opts...)
if err != nil {
errMsg := err.Error()
if err == tele.ErrMessageNotModified || strings.Contains(errMsg, "message is not modified") {
return nil
}
if strings.Contains(errMsg, "message to edit not found") {
return m.sendNewText(chat, content, markup)
}
return err
}
return nil
}
func (m *Manager) sendNewText(chat *tele.Chat, content string, markup *tele.ReplyMarkup) error {
opts := []interface{}{tele.ModeHTML, tele.NoPreview}
if markup != nil {
opts = append(opts, markup)
}
msg, err := m.bot.Send(chat, content, opts...)
if err != nil {
return err
}
return m.storage.SetTocMsgID(msg.ID)
}
func (m *Manager) updateWithPhoto(chat *tele.Chat, msgID int, content string, markup *tele.ReplyMarkup) error {
photo := &tele.Photo{
File: tele.FromDisk(m.coverImage),
Caption: content,
}
if msgID == 0 {
opts := []interface{}{tele.ModeHTML}
if markup != nil {
opts = append(opts, markup)
}
msg, err := m.bot.Send(chat, photo, opts...)
if err != nil {
return err
}
return m.storage.SetTocMsgID(msg.ID)
}
existingMsg := &tele.Message{
ID: msgID,
Chat: chat,
}
// 编辑图片消息的 caption
opts := []interface{}{tele.ModeHTML}
if markup != nil {
opts = append(opts, markup)
}
_, err := m.bot.EditCaption(existingMsg, content, opts...)
if err != nil {
errMsg := err.Error()
if err == tele.ErrMessageNotModified || strings.Contains(errMsg, "message is not modified") {
return nil
}
if strings.Contains(errMsg, "message to edit not found") ||
strings.Contains(errMsg, "no caption") {
opts := []interface{}{tele.ModeHTML}
if markup != nil {
opts = append(opts, markup)
}
msg, err := m.bot.Send(chat, photo, opts...)
if err != nil {
return err
}
return m.storage.SetTocMsgID(msg.ID)
}
return err
}
return nil
}
func (m *Manager) Stop() {
m.mu.Lock()
defer m.mu.Unlock()
if m.timer != nil {
m.timer.Stop()
}
close(m.stopCh)
}