Files
mygoTgChanBot/internal/telegram/handlers_post.go

452 lines
12 KiB
Go

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
}
// 只处理有活跃投稿状态的用户
state := b.states.Get(c.Sender().ID)
if state == nil {
return nil
}
// 转发消息处理 (StepAwaitForward)
// OriginalChat: 转发自频道/群组; OriginalSender: 转发自个人用户
isForwarded := msg.OriginalChat != nil || msg.OriginalSender != nil
if isForwarded && state.Step == StepAwaitForward {
return b.handleForwarded(c)
}
// 自定义标题输入 (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)
}