Files
mygoTgChanBot/internal/telegram/handlers_post.go
2026-02-05 10:40:17 +08:00

456 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package telegram
import (
"fmt"
"strings"
"tgchanbot/internal/storage"
tele "gopkg.in/telebot.v3"
)
const (
cbPrefixCat = "cat:"
cbPrefixConfirm = "confirm:"
cbPrefixTitle = "title:"
cbCancel = "cancel"
)
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 || state.Step == StepAwaitForward {
b.states.StartPost(c.Sender().ID)
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("❌ 这不是一条转发消息,请转发一条消息给我")
}
b.states.SetForwarded(c.Sender().ID, msg)
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 创建分类")
}
keyboard := b.buildCategoryKeyboard(categories)
return c.Reply("📁 请选择分类:", 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)
}
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)
msg := state.ForwardedMsg
title := state.Title
// 复制消息内容发送到频道(非转发,可编辑)
channel := &tele.Chat{ID: b.cfg.Channel.ID}
channelMsg, err := b.sendMessageCopy(channel, msg)
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("不支持的消息类型")
}
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)
}