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 } // 检查是否为转发消息 isForwarded := msg.OriginalChat != nil || msg.OriginalSender != nil 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() // 检查是否为转发消息(来自频道/群组或个人用户) if msg.OriginalChat == nil && msg.OriginalSender == nil { 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 := buildChannelLink(b.cfg.Channel.ID, 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 复制消息内容发送(非转发,可编辑) func (b *Bot) sendMessageCopy(to *tele.Chat, msg *tele.Message) (*tele.Message, error) { // 图片 if msg.Photo != nil { photo := &tele.Photo{File: msg.Photo.File, Caption: msg.Caption} return b.bot.Send(to, photo, tele.ModeHTML) } // 视频 if msg.Video != nil { video := &tele.Video{File: msg.Video.File, Caption: msg.Caption} return b.bot.Send(to, video, tele.ModeHTML) } // 文档 if msg.Document != nil { doc := &tele.Document{File: msg.Document.File, Caption: msg.Caption} return b.bot.Send(to, doc, tele.ModeHTML) } // 音频 if msg.Audio != nil { audio := &tele.Audio{File: msg.Audio.File, Caption: msg.Caption} return b.bot.Send(to, audio, tele.ModeHTML) } // 语音 if msg.Voice != nil { voice := &tele.Voice{File: msg.Voice.File} return b.bot.Send(to, voice) } // 贴纸 if msg.Sticker != nil { sticker := &tele.Sticker{File: msg.Sticker.File} return b.bot.Send(to, sticker) } // 动图 if msg.Animation != nil { anim := &tele.Animation{File: msg.Animation.File, Caption: msg.Caption} return b.bot.Send(to, anim, tele.ModeHTML) } // 纯文本 if msg.Text != "" { return b.bot.Send(to, msg.Text, tele.ModeHTML, tele.NoPreview) } return nil, fmt.Errorf("不支持的消息类型") } // sendAlbumCopy 复制相册发送到频道,返回第一条消息 func (b *Bot) sendAlbumCopy(to *tele.Chat, msgs []*tele.Message) (*tele.Message, error) { if len(msgs) == 0 { return nil, fmt.Errorf("相册为空") } var album tele.Album for i, msg := range msgs { var caption string if i == 0 { // 只有第一条消息带 caption caption = msg.Caption } if msg.Photo != nil { album = append(album, &tele.Photo{File: msg.Photo.File, Caption: caption}) } else if msg.Video != nil { album = append(album, &tele.Video{File: msg.Video.File, Caption: caption}) } else if msg.Document != nil { album = append(album, &tele.Document{File: msg.Document.File, Caption: caption}) } else if msg.Audio != nil { album = append(album, &tele.Audio{File: msg.Audio.File, Caption: caption}) } } if len(album) == 0 { return nil, fmt.Errorf("相册中没有支持的媒体类型") } sentMsgs, err := b.bot.SendAlbum(to, album, tele.ModeHTML) if err != nil { return nil, err } if len(sentMsgs) == 0 { return nil, fmt.Errorf("发送相册失败") } return &sentMsgs[0], nil } func buildChannelLink(channelID int64, msgID int) string { chatID := channelID 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) }