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) }