From 6686a43a35f6b60dd69727047baa6924ac0a61fe Mon Sep 17 00:00:00 2001 From: zuimao_kaf Date: Tue, 17 Mar 2026 13:14:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8A=A8=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E8=87=AA=E5=8A=A8=E8=AF=86=E5=88=AB=E8=BD=AC?= =?UTF-8?q?=E5=8F=91=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/telegram/handlers_post.go | 1084 ++++++++++++++-------------- 1 file changed, 542 insertions(+), 542 deletions(-) diff --git a/internal/telegram/handlers_post.go b/internal/telegram/handlers_post.go index 77adf73..4090856 100644 --- a/internal/telegram/handlers_post.go +++ b/internal/telegram/handlers_post.go @@ -1,542 +1,542 @@ -package telegram - -import ( - "fmt" - "strings" - "sync" - "time" - - "tgchanbot/internal/storage" - - tele "gopkg.in/telebot.v3" -) - -const ( - cbPrefixCat = "cat:" - cbPrefixConfirm = "confirm:" - cbPrefixTitle = "title:" - cbCancel = "cancel" - - albumCollectDelay = 500 * time.Millisecond // 等待相册其他消息的时间 -) - -// 相册收集定时器 -var ( - albumTimers = make(map[int64]*time.Timer) - albumTimersMu sync.Mutex -) - -func (b *Bot) handlePost(c tele.Context) error { - msg := c.Message() - payload := strings.TrimSpace(c.Message().Payload) - - // 快捷方式: 回复消息 + /post <分类> /tt <标题> - if msg.ReplyTo != nil && payload != "" { - return b.handleQuickPost(c, msg.ReplyTo, payload) - } - - // 常规交互流程 - b.states.StartPost(c.Sender().ID) - return c.Reply("📨 请转发需要归档到目录的消息\n\n消息将被同步发送到频道并添加到目录\n\n💡 快捷方式: 回复消息并发送\n`/post <分类> /tt <标题>`", tele.ModeMarkdown) -} - -func (b *Bot) handleQuickPost(c tele.Context, replyMsg *tele.Message, payload string) error { - // 解析: <分类> /tt <标题> - parts := strings.SplitN(payload, "/tt", 2) - category := strings.TrimSpace(parts[0]) - - var title string - if len(parts) > 1 { - title = strings.TrimSpace(parts[1]) - } - - if category == "" { - return c.Reply("❌ 用法: /post <分类> /tt <标题>\n例如: /post iOS /tt 某个APP推荐") - } - - // 验证分类存在 - if !b.storage.CategoryExists(category) { - categories, _ := b.storage.ListCategories() - var names []string - for _, cat := range categories { - names = append(names, cat.Name) - } - return c.Reply(fmt.Sprintf("❌ 分类 [%s] 不存在\n\n可用分类: %s", category, strings.Join(names, ", "))) - } - - // 标题: 优先使用指定标题,否则从回复消息提取 - if title == "" { - title = extractTitle(replyMsg) - } - - // 构建链接 - link := buildMessageLinkFromReply(c.Message(), replyMsg) - - // 创建条目 - entry, err := b.storage.CreateEntry(category, title, link) - if err != nil { - return c.Reply(fmt.Sprintf("❌ 保存失败: %v", err)) - } - - b.toc.TriggerUpdate() - - return c.Reply(fmt.Sprintf("✅ 已添加\n\nID: `%s`\n分类: %s\n标题: %s\n链接: %s", - entry.ID, entry.Category, entry.Title, entry.Link), tele.ModeMarkdown) -} - -func buildMessageLinkFromReply(currentMsg, replyMsg *tele.Message) string { - chat := currentMsg.Chat - msgID := replyMsg.ID - - if chat.Username != "" { - return fmt.Sprintf("https://t.me/%s/%d", chat.Username, msgID) - } - - chatID := chat.ID - if chatID < 0 { - chatID = -chatID - 1000000000000 - } - return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID) -} - -func (b *Bot) handleTextInput(c tele.Context) error { - msg := c.Message() - if msg == nil { - return nil - } - - // 只处理私聊消息,忽略群组对话 - if c.Chat().Type != tele.ChatPrivate { - return nil - } - - // 检查是否为转发消息(OriginalUnixtime 在隐私设置隐藏来源时仍存在) - isForwarded := msg.OriginalChat != nil || msg.OriginalSender != nil || msg.OriginalUnixtime > 0 - - state := b.states.Get(c.Sender().ID) - - // 私聊收到转发消息,直接启动投稿流程(无需先 /post) - if isForwarded && b.cfg.IsAdmin(c.Sender().ID) { - if state == nil { - b.states.StartPost(c.Sender().ID) - } - if state == nil || state.Step == StepAwaitForward { - return b.handleForwarded(c) - } - } - - if state == nil { - return nil - } - - // 自定义标题输入 (StepAwaitTitle) - if state.Step == StepAwaitTitle { - return b.handleTitleInput(c) - } - - return nil -} - -func (b *Bot) handleForwarded(c tele.Context) error { - if !b.cfg.IsAdmin(c.Sender().ID) { - return nil - } - - state := b.states.Get(c.Sender().ID) - if state == nil || state.Step != StepAwaitForward { - return nil - } - - msg := c.Message() - // 检查是否为转发消息(OriginalUnixtime 在隐私设置隐藏来源时仍存在) - if msg.OriginalChat == nil && msg.OriginalSender == nil && msg.OriginalUnixtime == 0 { - return c.Reply("❌ 这不是一条转发消息,请转发一条消息给我") - } - - userID := c.Sender().ID - - // 相册消息处理 - if msg.AlbumID != "" { - b.states.AddAlbumMessage(userID, msg) - b.scheduleAlbumFinalize(c, userID) - return nil // 等待收集完成 - } - - // 单条消息 - b.states.SetForwarded(userID, msg) - return b.showCategorySelection(c) -} - -// scheduleAlbumFinalize 延迟完成相册收集 -func (b *Bot) scheduleAlbumFinalize(c tele.Context, userID int64) { - albumTimersMu.Lock() - defer albumTimersMu.Unlock() - - // 取消之前的定时器 - if timer, exists := albumTimers[userID]; exists { - timer.Stop() - } - - // 设置新定时器 - albumTimers[userID] = time.AfterFunc(albumCollectDelay, func() { - albumTimersMu.Lock() - delete(albumTimers, userID) - albumTimersMu.Unlock() - - b.states.FinalizeAlbum(userID) - b.showCategorySelectionAsync(c, userID) - }) -} - -// showCategorySelection 显示分类选择 -func (b *Bot) showCategorySelection(c tele.Context) error { - categories, err := b.storage.ListCategories() - if err != nil { - b.states.Delete(c.Sender().ID) - return c.Reply(fmt.Sprintf("❌ 获取分类失败: %v", err)) - } - - if len(categories) == 0 { - b.states.Delete(c.Sender().ID) - return c.Reply("❌ 暂无分类,请先使用 /cat_add 创建分类") - } - - state := b.states.Get(c.Sender().ID) - msgCount := 1 - if state != nil && len(state.ForwardedMsgs) > 0 { - msgCount = len(state.ForwardedMsgs) - } - - keyboard := b.buildCategoryKeyboard(categories) - if msgCount > 1 { - return c.Reply(fmt.Sprintf("📁 已收到 %d 条消息(相册),请选择分类:", msgCount), keyboard) - } - return c.Reply("📁 请选择分类:", keyboard) -} - -// showCategorySelectionAsync 异步显示分类选择(用于定时器回调) -func (b *Bot) showCategorySelectionAsync(c tele.Context, userID int64) { - categories, err := b.storage.ListCategories() - if err != nil { - b.states.Delete(userID) - c.Bot().Send(c.Chat(), fmt.Sprintf("❌ 获取分类失败: %v", err)) - return - } - - if len(categories) == 0 { - b.states.Delete(userID) - c.Bot().Send(c.Chat(), "❌ 暂无分类,请先使用 /cat_add 创建分类") - return - } - - state := b.states.Get(userID) - msgCount := 1 - if state != nil && len(state.ForwardedMsgs) > 0 { - msgCount = len(state.ForwardedMsgs) - } - - keyboard := b.buildCategoryKeyboard(categories) - if msgCount > 1 { - c.Bot().Send(c.Chat(), fmt.Sprintf("📁 已收到 %d 条消息(相册),请选择分类:", msgCount), keyboard) - } else { - c.Bot().Send(c.Chat(), "📁 请选择分类:", keyboard) - } -} - -func (b *Bot) buildCategoryKeyboard(categories []storage.Category) *tele.ReplyMarkup { - menu := &tele.ReplyMarkup{} - var rows []tele.Row - - var currentRow []tele.Btn - for _, cat := range categories { - btn := menu.Data(cat.Name, cbPrefixCat+cat.Name) - currentRow = append(currentRow, btn) - - if len(currentRow) == 3 { - rows = append(rows, menu.Row(currentRow...)) - currentRow = nil - } - } - - if len(currentRow) > 0 { - rows = append(rows, menu.Row(currentRow...)) - } - - cancelBtn := menu.Data("❌ 取消", cbCancel) - rows = append(rows, menu.Row(cancelBtn)) - - menu.Inline(rows...) - return menu -} - -func (b *Bot) handleCallback(c tele.Context) error { - if !b.cfg.IsAdmin(c.Sender().ID) { - return c.Respond(&tele.CallbackResponse{Text: "无权限"}) - } - - // telebot v3 会在 data 前加 \f 前缀,需要去掉 - data := strings.TrimPrefix(c.Callback().Data, "\f") - userID := c.Sender().ID - - switch { - case data == cbCancel: - return b.handleCancelCallback(c, userID) - - case strings.HasPrefix(data, cbPrefixCat): - category := strings.TrimPrefix(data, cbPrefixCat) - return b.handleCategoryCallback(c, userID, category) - - case strings.HasPrefix(data, cbPrefixTitle): - action := strings.TrimPrefix(data, cbPrefixTitle) - return b.handleTitleCallback(c, userID, action) - - case strings.HasPrefix(data, cbPrefixConfirm): - action := strings.TrimPrefix(data, cbPrefixConfirm) - return b.handleConfirmCallback(c, userID, action) - - case strings.HasPrefix(data, cbPrefixDelWithMsg): - entryID := strings.TrimPrefix(data, cbPrefixDelWithMsg) - return b.handleDeleteEntryCallback(c, entryID, true) - - case strings.HasPrefix(data, cbPrefixDelOnlyToc): - entryID := strings.TrimPrefix(data, cbPrefixDelOnlyToc) - return b.handleDeleteEntryCallback(c, entryID, false) - } - - return c.Respond() -} - -func (b *Bot) handleCancelCallback(c tele.Context, userID int64) error { - b.states.Delete(userID) - c.Edit("❌ 已取消") - return c.Respond(&tele.CallbackResponse{Text: "已取消"}) -} - -func (b *Bot) handleTitleCallback(c tele.Context, userID int64, action string) error { - state := b.states.Get(userID) - if state == nil || state.Step != StepAwaitTitle { - return c.Respond(&tele.CallbackResponse{Text: "会话已过期"}) - } - - if action == "default" { - // 使用默认标题 - title := extractTitle(state.ForwardedMsg) - b.states.SetTitle(userID, title) - return b.showConfirmation(c, state, title) - } - - // 自定义标题 - 提示用户输入 - c.Edit("✏️ 请发送新标题:") - return c.Respond() -} - -func (b *Bot) handleTitleInput(c tele.Context) error { - state := b.states.Get(c.Sender().ID) - if state == nil || state.Step != StepAwaitTitle { - return nil - } - - title := strings.TrimSpace(c.Message().Text) - if title == "" { - return c.Reply("❌ 标题不能为空,请重新输入:") - } - - if len(title) > 50 { - title = title[:47] + "..." - } - - b.states.SetTitle(c.Sender().ID, title) - return b.showConfirmationMsg(c, state, title) -} - -func (b *Bot) showConfirmation(c tele.Context, state *PostState, title string) error { - channelName := "未知频道" - if state.ForwardedMsg.OriginalChat != nil { - channelName = state.ForwardedMsg.OriginalChat.Title - } - - menu := &tele.ReplyMarkup{} - confirmBtn := menu.Data("✅ 确认添加", cbPrefixConfirm+"yes") - cancelBtn := menu.Data("❌ 取消", cbCancel) - menu.Inline(menu.Row(confirmBtn, cancelBtn)) - - text := fmt.Sprintf("📋 确认添加?\n\n频道: %s\n分类: %s\n标题: %s", channelName, state.SelectedCat, title) - c.Edit(text, menu) - return c.Respond() -} - -func (b *Bot) showConfirmationMsg(c tele.Context, state *PostState, title string) error { - channelName := "未知频道" - if state.ForwardedMsg.OriginalChat != nil { - channelName = state.ForwardedMsg.OriginalChat.Title - } - - menu := &tele.ReplyMarkup{} - confirmBtn := menu.Data("✅ 确认添加", cbPrefixConfirm+"yes") - cancelBtn := menu.Data("❌ 取消", cbCancel) - menu.Inline(menu.Row(confirmBtn, cancelBtn)) - - text := fmt.Sprintf("📋 确认添加?\n\n频道: %s\n分类: %s\n标题: %s", channelName, state.SelectedCat, title) - return c.Reply(text, menu) -} - -func (b *Bot) handleCategoryCallback(c tele.Context, userID int64, category string) error { - state := b.states.Get(userID) - if state == nil || state.Step != StepAwaitCategory { - return c.Respond(&tele.CallbackResponse{Text: "会话已过期"}) - } - - b.states.SetCategory(userID, category) - - // 提取默认标题 - defaultTitle := extractTitle(state.ForwardedMsg) - - menu := &tele.ReplyMarkup{} - useDefaultBtn := menu.Data("✅ 使用此标题", cbPrefixTitle+"default") - customBtn := menu.Data("✏️ 自定义标题", cbPrefixTitle+"custom") - cancelBtn := menu.Data("❌ 取消", cbCancel) - menu.Inline( - menu.Row(useDefaultBtn), - menu.Row(customBtn), - menu.Row(cancelBtn), - ) - - text := fmt.Sprintf("📝 确认标题\n\n分类: %s\n标题: %s\n\n使用此标题或自定义?", category, defaultTitle) - c.Edit(text, menu) - return c.Respond() -} - -func (b *Bot) handleConfirmCallback(c tele.Context, userID int64, action string) error { - if action != "yes" { - return b.handleCancelCallback(c, userID) - } - - state := b.states.Get(userID) - if state == nil || state.Step != StepAwaitConfirm { - return c.Respond(&tele.CallbackResponse{Text: "会话已过期"}) - } - - defer b.states.Delete(userID) - - title := state.Title - channel := &tele.Chat{ID: b.cfg.Channel.ID} - - var channelMsg *tele.Message - var err error - - // 相册消息 - if len(state.ForwardedMsgs) > 1 { - channelMsg, err = b.sendAlbumCopy(channel, state.ForwardedMsgs) - } else { - // 单条消息 - channelMsg, err = b.sendMessageCopy(channel, state.ForwardedMsg) - } - - if err != nil { - c.Edit(fmt.Sprintf("❌ 发送到频道失败: %v", err)) - return c.Respond(&tele.CallbackResponse{Text: "发送失败"}) - } - - // 使用频道消息的链接 - link := b.buildChannelLink(channelMsg.ID) - - entry, err := b.storage.CreateEntry(state.SelectedCat, title, link) - if err != nil { - c.Edit(fmt.Sprintf("❌ 保存失败: %v", err)) - return c.Respond(&tele.CallbackResponse{Text: "保存失败"}) - } - - b.toc.TriggerUpdate() - - text := fmt.Sprintf("✅ 已添加\n\nID: %s\n分类: %s\n标题: %s", entry.ID, entry.Category, entry.Title) - c.Edit(text) - return c.Respond(&tele.CallbackResponse{Text: "添加成功"}) -} - -// sendMessageCopy 复制消息内容发送(使用 copyMessage API 保留原格式) -func (b *Bot) sendMessageCopy(to *tele.Chat, msg *tele.Message) (*tele.Message, error) { - // 使用 Copy 方法,完整保留原消息的 entities(代码块、粗体、斜体等格式) - return b.bot.Copy(to, msg) -} - -// sendAlbumCopy 复制相册发送到频道,返回第一条消息 -func (b *Bot) sendAlbumCopy(to *tele.Chat, msgs []*tele.Message) (*tele.Message, error) { - if len(msgs) == 0 { - return nil, fmt.Errorf("相册为空") - } - - // 转换为 Editable 接口切片 - editables := make([]tele.Editable, len(msgs)) - for i, msg := range msgs { - editables[i] = msg - } - - // 使用 CopyMany 保留原格式(代码块、粗体等 entities) - sentMsgs, err := b.bot.CopyMany(to, editables) - if err != nil { - return nil, err - } - - if len(sentMsgs) == 0 { - return nil, fmt.Errorf("发送相册失败") - } - - return &sentMsgs[0], nil -} - -func (b *Bot) buildChannelLink(msgID int) string { - // 尝试获取频道信息,检查是否为公开频道 - chat, err := b.bot.ChatByID(b.cfg.Channel.ID) - if err == nil && chat.Username != "" { - return fmt.Sprintf("https://t.me/%s/%d", chat.Username, msgID) - } - - // 私有频道 fallback - chatID := b.cfg.Channel.ID - if chatID < 0 { - chatID = -chatID - 1000000000000 - } - return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID) -} - -func extractTitle(msg *tele.Message) string { - text := msg.Text - if text == "" { - text = msg.Caption - } - - lines := strings.Split(text, "\n") - title := strings.TrimSpace(lines[0]) - - if len(title) > 50 { - title = title[:47] + "..." - } - - if title == "" { - title = "无标题" - } - - return title -} - -func buildMessageLink(msg *tele.Message) string { - if msg.OriginalChat == nil { - return "" - } - - chat := msg.OriginalChat - msgID := msg.OriginalMessageID - if msgID == 0 { - msgID = msg.ID - } - - if chat.Username != "" { - return fmt.Sprintf("https://t.me/%s/%d", chat.Username, msgID) - } - - chatID := chat.ID - if chatID < 0 { - chatID = -chatID - 1000000000000 - } - return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID) -} +package telegram + +import ( + "fmt" + "strings" + "sync" + "time" + + "tgchanbot/internal/storage" + + tele "gopkg.in/telebot.v3" +) + +const ( + cbPrefixCat = "cat:" + cbPrefixConfirm = "confirm:" + cbPrefixTitle = "title:" + cbCancel = "cancel" + + albumCollectDelay = 500 * time.Millisecond // 等待相册其他消息的时间 +) + +// 相册收集定时器 +var ( + albumTimers = make(map[int64]*time.Timer) + albumTimersMu sync.Mutex +) + +func (b *Bot) handlePost(c tele.Context) error { + msg := c.Message() + payload := strings.TrimSpace(c.Message().Payload) + + // 快捷方式: 回复消息 + /post <分类> /tt <标题> + if msg.ReplyTo != nil && payload != "" { + return b.handleQuickPost(c, msg.ReplyTo, payload) + } + + // 常规交互流程 + b.states.StartPost(c.Sender().ID) + return c.Reply("📨 请转发需要归档到目录的消息\n\n消息将被同步发送到频道并添加到目录\n\n💡 快捷方式: 回复消息并发送\n`/post <分类> /tt <标题>`", tele.ModeMarkdown) +} + +func (b *Bot) handleQuickPost(c tele.Context, replyMsg *tele.Message, payload string) error { + // 解析: <分类> /tt <标题> + parts := strings.SplitN(payload, "/tt", 2) + category := strings.TrimSpace(parts[0]) + + var title string + if len(parts) > 1 { + title = strings.TrimSpace(parts[1]) + } + + if category == "" { + return c.Reply("❌ 用法: /post <分类> /tt <标题>\n例如: /post iOS /tt 某个APP推荐") + } + + // 验证分类存在 + if !b.storage.CategoryExists(category) { + categories, _ := b.storage.ListCategories() + var names []string + for _, cat := range categories { + names = append(names, cat.Name) + } + return c.Reply(fmt.Sprintf("❌ 分类 [%s] 不存在\n\n可用分类: %s", category, strings.Join(names, ", "))) + } + + // 标题: 优先使用指定标题,否则从回复消息提取 + if title == "" { + title = extractTitle(replyMsg) + } + + // 构建链接 + link := buildMessageLinkFromReply(c.Message(), replyMsg) + + // 创建条目 + entry, err := b.storage.CreateEntry(category, title, link) + if err != nil { + return c.Reply(fmt.Sprintf("❌ 保存失败: %v", err)) + } + + b.toc.TriggerUpdate() + + return c.Reply(fmt.Sprintf("✅ 已添加\n\nID: `%s`\n分类: %s\n标题: %s\n链接: %s", + entry.ID, entry.Category, entry.Title, entry.Link), tele.ModeMarkdown) +} + +func buildMessageLinkFromReply(currentMsg, replyMsg *tele.Message) string { + chat := currentMsg.Chat + msgID := replyMsg.ID + + if chat.Username != "" { + return fmt.Sprintf("https://t.me/%s/%d", chat.Username, msgID) + } + + chatID := chat.ID + if chatID < 0 { + chatID = -chatID - 1000000000000 + } + return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID) +} + +func (b *Bot) handleTextInput(c tele.Context) error { + msg := c.Message() + if msg == nil { + return nil + } + + // 只处理私聊消息,忽略群组对话 + if c.Chat().Type != tele.ChatPrivate { + return nil + } + + // 检查是否为转发消息(OriginalUnixtime 在隐私设置隐藏来源时仍存在) + isForwarded := msg.OriginalChat != nil || msg.OriginalSender != nil || msg.OriginalUnixtime > 0 + + state := b.states.Get(c.Sender().ID) + + // 私聊收到转发消息,直接启动投稿流程(无需先 /post) + if isForwarded && b.isAdmin(c.Sender().ID) { + if state == nil { + b.states.StartPost(c.Sender().ID) + } + if state == nil || state.Step == StepAwaitForward { + return b.handleForwarded(c) + } + } + + if state == nil { + return nil + } + + // 自定义标题输入 (StepAwaitTitle) + if state.Step == StepAwaitTitle { + return b.handleTitleInput(c) + } + + return nil +} + +func (b *Bot) handleForwarded(c tele.Context) error { + if !b.isAdmin(c.Sender().ID) { + return nil + } + + state := b.states.Get(c.Sender().ID) + if state == nil || state.Step != StepAwaitForward { + return nil + } + + msg := c.Message() + // 检查是否为转发消息(OriginalUnixtime 在隐私设置隐藏来源时仍存在) + if msg.OriginalChat == nil && msg.OriginalSender == nil && msg.OriginalUnixtime == 0 { + return c.Reply("❌ 这不是一条转发消息,请转发一条消息给我") + } + + userID := c.Sender().ID + + // 相册消息处理 + if msg.AlbumID != "" { + b.states.AddAlbumMessage(userID, msg) + b.scheduleAlbumFinalize(c, userID) + return nil // 等待收集完成 + } + + // 单条消息 + b.states.SetForwarded(userID, msg) + return b.showCategorySelection(c) +} + +// scheduleAlbumFinalize 延迟完成相册收集 +func (b *Bot) scheduleAlbumFinalize(c tele.Context, userID int64) { + albumTimersMu.Lock() + defer albumTimersMu.Unlock() + + // 取消之前的定时器 + if timer, exists := albumTimers[userID]; exists { + timer.Stop() + } + + // 设置新定时器 + albumTimers[userID] = time.AfterFunc(albumCollectDelay, func() { + albumTimersMu.Lock() + delete(albumTimers, userID) + albumTimersMu.Unlock() + + b.states.FinalizeAlbum(userID) + b.showCategorySelectionAsync(c, userID) + }) +} + +// showCategorySelection 显示分类选择 +func (b *Bot) showCategorySelection(c tele.Context) error { + categories, err := b.storage.ListCategories() + if err != nil { + b.states.Delete(c.Sender().ID) + return c.Reply(fmt.Sprintf("❌ 获取分类失败: %v", err)) + } + + if len(categories) == 0 { + b.states.Delete(c.Sender().ID) + return c.Reply("❌ 暂无分类,请先使用 /cat_add 创建分类") + } + + state := b.states.Get(c.Sender().ID) + msgCount := 1 + if state != nil && len(state.ForwardedMsgs) > 0 { + msgCount = len(state.ForwardedMsgs) + } + + keyboard := b.buildCategoryKeyboard(categories) + if msgCount > 1 { + return c.Reply(fmt.Sprintf("📁 已收到 %d 条消息(相册),请选择分类:", msgCount), keyboard) + } + return c.Reply("📁 请选择分类:", keyboard) +} + +// showCategorySelectionAsync 异步显示分类选择(用于定时器回调) +func (b *Bot) showCategorySelectionAsync(c tele.Context, userID int64) { + categories, err := b.storage.ListCategories() + if err != nil { + b.states.Delete(userID) + c.Bot().Send(c.Chat(), fmt.Sprintf("❌ 获取分类失败: %v", err)) + return + } + + if len(categories) == 0 { + b.states.Delete(userID) + c.Bot().Send(c.Chat(), "❌ 暂无分类,请先使用 /cat_add 创建分类") + return + } + + state := b.states.Get(userID) + msgCount := 1 + if state != nil && len(state.ForwardedMsgs) > 0 { + msgCount = len(state.ForwardedMsgs) + } + + keyboard := b.buildCategoryKeyboard(categories) + if msgCount > 1 { + c.Bot().Send(c.Chat(), fmt.Sprintf("📁 已收到 %d 条消息(相册),请选择分类:", msgCount), keyboard) + } else { + c.Bot().Send(c.Chat(), "📁 请选择分类:", keyboard) + } +} + +func (b *Bot) buildCategoryKeyboard(categories []storage.Category) *tele.ReplyMarkup { + menu := &tele.ReplyMarkup{} + var rows []tele.Row + + var currentRow []tele.Btn + for _, cat := range categories { + btn := menu.Data(cat.Name, cbPrefixCat+cat.Name) + currentRow = append(currentRow, btn) + + if len(currentRow) == 3 { + rows = append(rows, menu.Row(currentRow...)) + currentRow = nil + } + } + + if len(currentRow) > 0 { + rows = append(rows, menu.Row(currentRow...)) + } + + cancelBtn := menu.Data("❌ 取消", cbCancel) + rows = append(rows, menu.Row(cancelBtn)) + + menu.Inline(rows...) + return menu +} + +func (b *Bot) handleCallback(c tele.Context) error { + if !b.isAdmin(c.Sender().ID) { + return c.Respond(&tele.CallbackResponse{Text: "无权限"}) + } + + // telebot v3 会在 data 前加 \f 前缀,需要去掉 + data := strings.TrimPrefix(c.Callback().Data, "\f") + userID := c.Sender().ID + + switch { + case data == cbCancel: + return b.handleCancelCallback(c, userID) + + case strings.HasPrefix(data, cbPrefixCat): + category := strings.TrimPrefix(data, cbPrefixCat) + return b.handleCategoryCallback(c, userID, category) + + case strings.HasPrefix(data, cbPrefixTitle): + action := strings.TrimPrefix(data, cbPrefixTitle) + return b.handleTitleCallback(c, userID, action) + + case strings.HasPrefix(data, cbPrefixConfirm): + action := strings.TrimPrefix(data, cbPrefixConfirm) + return b.handleConfirmCallback(c, userID, action) + + case strings.HasPrefix(data, cbPrefixDelWithMsg): + entryID := strings.TrimPrefix(data, cbPrefixDelWithMsg) + return b.handleDeleteEntryCallback(c, entryID, true) + + case strings.HasPrefix(data, cbPrefixDelOnlyToc): + entryID := strings.TrimPrefix(data, cbPrefixDelOnlyToc) + return b.handleDeleteEntryCallback(c, entryID, false) + } + + return c.Respond() +} + +func (b *Bot) handleCancelCallback(c tele.Context, userID int64) error { + b.states.Delete(userID) + c.Edit("❌ 已取消") + return c.Respond(&tele.CallbackResponse{Text: "已取消"}) +} + +func (b *Bot) handleTitleCallback(c tele.Context, userID int64, action string) error { + state := b.states.Get(userID) + if state == nil || state.Step != StepAwaitTitle { + return c.Respond(&tele.CallbackResponse{Text: "会话已过期"}) + } + + if action == "default" { + // 使用默认标题 + title := extractTitle(state.ForwardedMsg) + b.states.SetTitle(userID, title) + return b.showConfirmation(c, state, title) + } + + // 自定义标题 - 提示用户输入 + c.Edit("✏️ 请发送新标题:") + return c.Respond() +} + +func (b *Bot) handleTitleInput(c tele.Context) error { + state := b.states.Get(c.Sender().ID) + if state == nil || state.Step != StepAwaitTitle { + return nil + } + + title := strings.TrimSpace(c.Message().Text) + if title == "" { + return c.Reply("❌ 标题不能为空,请重新输入:") + } + + if len(title) > 50 { + title = title[:47] + "..." + } + + b.states.SetTitle(c.Sender().ID, title) + return b.showConfirmationMsg(c, state, title) +} + +func (b *Bot) showConfirmation(c tele.Context, state *PostState, title string) error { + channelName := "未知频道" + if state.ForwardedMsg.OriginalChat != nil { + channelName = state.ForwardedMsg.OriginalChat.Title + } + + menu := &tele.ReplyMarkup{} + confirmBtn := menu.Data("✅ 确认添加", cbPrefixConfirm+"yes") + cancelBtn := menu.Data("❌ 取消", cbCancel) + menu.Inline(menu.Row(confirmBtn, cancelBtn)) + + text := fmt.Sprintf("📋 确认添加?\n\n频道: %s\n分类: %s\n标题: %s", channelName, state.SelectedCat, title) + c.Edit(text, menu) + return c.Respond() +} + +func (b *Bot) showConfirmationMsg(c tele.Context, state *PostState, title string) error { + channelName := "未知频道" + if state.ForwardedMsg.OriginalChat != nil { + channelName = state.ForwardedMsg.OriginalChat.Title + } + + menu := &tele.ReplyMarkup{} + confirmBtn := menu.Data("✅ 确认添加", cbPrefixConfirm+"yes") + cancelBtn := menu.Data("❌ 取消", cbCancel) + menu.Inline(menu.Row(confirmBtn, cancelBtn)) + + text := fmt.Sprintf("📋 确认添加?\n\n频道: %s\n分类: %s\n标题: %s", channelName, state.SelectedCat, title) + return c.Reply(text, menu) +} + +func (b *Bot) handleCategoryCallback(c tele.Context, userID int64, category string) error { + state := b.states.Get(userID) + if state == nil || state.Step != StepAwaitCategory { + return c.Respond(&tele.CallbackResponse{Text: "会话已过期"}) + } + + b.states.SetCategory(userID, category) + + // 提取默认标题 + defaultTitle := extractTitle(state.ForwardedMsg) + + menu := &tele.ReplyMarkup{} + useDefaultBtn := menu.Data("✅ 使用此标题", cbPrefixTitle+"default") + customBtn := menu.Data("✏️ 自定义标题", cbPrefixTitle+"custom") + cancelBtn := menu.Data("❌ 取消", cbCancel) + menu.Inline( + menu.Row(useDefaultBtn), + menu.Row(customBtn), + menu.Row(cancelBtn), + ) + + text := fmt.Sprintf("📝 确认标题\n\n分类: %s\n标题: %s\n\n使用此标题或自定义?", category, defaultTitle) + c.Edit(text, menu) + return c.Respond() +} + +func (b *Bot) handleConfirmCallback(c tele.Context, userID int64, action string) error { + if action != "yes" { + return b.handleCancelCallback(c, userID) + } + + state := b.states.Get(userID) + if state == nil || state.Step != StepAwaitConfirm { + return c.Respond(&tele.CallbackResponse{Text: "会话已过期"}) + } + + defer b.states.Delete(userID) + + title := state.Title + channel := &tele.Chat{ID: b.cfg.Channel.ID} + + var channelMsg *tele.Message + var err error + + // 相册消息 + if len(state.ForwardedMsgs) > 1 { + channelMsg, err = b.sendAlbumCopy(channel, state.ForwardedMsgs) + } else { + // 单条消息 + channelMsg, err = b.sendMessageCopy(channel, state.ForwardedMsg) + } + + if err != nil { + c.Edit(fmt.Sprintf("❌ 发送到频道失败: %v", err)) + return c.Respond(&tele.CallbackResponse{Text: "发送失败"}) + } + + // 使用频道消息的链接 + link := b.buildChannelLink(channelMsg.ID) + + entry, err := b.storage.CreateEntry(state.SelectedCat, title, link) + if err != nil { + c.Edit(fmt.Sprintf("❌ 保存失败: %v", err)) + return c.Respond(&tele.CallbackResponse{Text: "保存失败"}) + } + + b.toc.TriggerUpdate() + + text := fmt.Sprintf("✅ 已添加\n\nID: %s\n分类: %s\n标题: %s", entry.ID, entry.Category, entry.Title) + c.Edit(text) + return c.Respond(&tele.CallbackResponse{Text: "添加成功"}) +} + +// sendMessageCopy 复制消息内容发送(使用 copyMessage API 保留原格式) +func (b *Bot) sendMessageCopy(to *tele.Chat, msg *tele.Message) (*tele.Message, error) { + // 使用 Copy 方法,完整保留原消息的 entities(代码块、粗体、斜体等格式) + return b.bot.Copy(to, msg) +} + +// sendAlbumCopy 复制相册发送到频道,返回第一条消息 +func (b *Bot) sendAlbumCopy(to *tele.Chat, msgs []*tele.Message) (*tele.Message, error) { + if len(msgs) == 0 { + return nil, fmt.Errorf("相册为空") + } + + // 转换为 Editable 接口切片 + editables := make([]tele.Editable, len(msgs)) + for i, msg := range msgs { + editables[i] = msg + } + + // 使用 CopyMany 保留原格式(代码块、粗体等 entities) + sentMsgs, err := b.bot.CopyMany(to, editables) + if err != nil { + return nil, err + } + + if len(sentMsgs) == 0 { + return nil, fmt.Errorf("发送相册失败") + } + + return &sentMsgs[0], nil +} + +func (b *Bot) buildChannelLink(msgID int) string { + // 尝试获取频道信息,检查是否为公开频道 + chat, err := b.bot.ChatByID(b.cfg.Channel.ID) + if err == nil && chat.Username != "" { + return fmt.Sprintf("https://t.me/%s/%d", chat.Username, msgID) + } + + // 私有频道 fallback + chatID := b.cfg.Channel.ID + if chatID < 0 { + chatID = -chatID - 1000000000000 + } + return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID) +} + +func extractTitle(msg *tele.Message) string { + text := msg.Text + if text == "" { + text = msg.Caption + } + + lines := strings.Split(text, "\n") + title := strings.TrimSpace(lines[0]) + + if len(title) > 50 { + title = title[:47] + "..." + } + + if title == "" { + title = "无标题" + } + + return title +} + +func buildMessageLink(msg *tele.Message) string { + if msg.OriginalChat == nil { + return "" + } + + chat := msg.OriginalChat + msgID := msg.OriginalMessageID + if msgID == 0 { + msgID = msg.ID + } + + if chat.Username != "" { + return fmt.Sprintf("https://t.me/%s/%d", chat.Username, msgID) + } + + chatID := chat.ID + if chatID < 0 { + chatID = -chatID - 1000000000000 + } + return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID) +}