目录保留消息上下文并增加直达链接
This commit is contained in:
@@ -1,172 +1,172 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tele "gopkg.in/telebot.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
cbPrefixDelEntry = "del:"
|
||||
cbPrefixDelWithMsg = "delwithmsg:"
|
||||
cbPrefixDelOnlyToc = "delonlytoc:"
|
||||
)
|
||||
|
||||
func (b *Bot) handleEntryDel(c tele.Context) error {
|
||||
id := strings.TrimSpace(c.Message().Payload)
|
||||
if id == "" {
|
||||
return c.Reply("用法: /del <ID>")
|
||||
}
|
||||
|
||||
entry, err := b.storage.GetEntry(id)
|
||||
if err != nil {
|
||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||
}
|
||||
|
||||
menu := &tele.ReplyMarkup{}
|
||||
delWithMsgBtn := menu.Data("🗑 删除条目+频道消息", cbPrefixDelWithMsg+id)
|
||||
delOnlyTocBtn := menu.Data("📝 仅删除目录条目", cbPrefixDelOnlyToc+id)
|
||||
cancelBtn := menu.Data("❌ 取消", cbCancel)
|
||||
menu.Inline(
|
||||
menu.Row(delWithMsgBtn),
|
||||
menu.Row(delOnlyTocBtn),
|
||||
menu.Row(cancelBtn),
|
||||
)
|
||||
|
||||
text := fmt.Sprintf("🗑 确认删除?\n\n分类: %s\n标题: %s\n\n请选择删除方式:", entry.Category, entry.Title)
|
||||
return c.Reply(text, menu)
|
||||
}
|
||||
|
||||
func (b *Bot) handleEntryEdit(c tele.Context) error {
|
||||
args := strings.SplitN(c.Message().Payload, " ", 2)
|
||||
if len(args) < 2 {
|
||||
return c.Reply("用法: /edit <ID> <新标题>")
|
||||
}
|
||||
|
||||
id := args[0]
|
||||
newTitle := strings.TrimSpace(args[1])
|
||||
|
||||
if err := b.storage.UpdateEntryTitle(id, newTitle); err != nil {
|
||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||
}
|
||||
|
||||
b.toc.TriggerUpdate()
|
||||
return c.Reply(fmt.Sprintf("✅ 标题已更新为: %s", newTitle))
|
||||
}
|
||||
|
||||
func (b *Bot) handleEntryMove(c tele.Context) error {
|
||||
args := strings.Fields(c.Message().Payload)
|
||||
if len(args) < 2 {
|
||||
return c.Reply("用法: /move <ID> <新分类>")
|
||||
}
|
||||
|
||||
id := args[0]
|
||||
newCategory := args[1]
|
||||
|
||||
if !b.storage.CategoryExists(newCategory) {
|
||||
return c.Reply(fmt.Sprintf("❌ 分类 [%s] 不存在", newCategory))
|
||||
}
|
||||
|
||||
if err := b.storage.UpdateEntryCategory(id, newCategory); err != nil {
|
||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||
}
|
||||
|
||||
b.toc.TriggerUpdate()
|
||||
return c.Reply(fmt.Sprintf("✅ 已移动到分类: %s", newCategory))
|
||||
}
|
||||
|
||||
func (b *Bot) handleEntryList(c tele.Context) error {
|
||||
category := strings.TrimSpace(c.Message().Payload)
|
||||
|
||||
entries, err := b.storage.ListEntries(category)
|
||||
if err != nil {
|
||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
if category != "" {
|
||||
return c.Reply(fmt.Sprintf("📭 分类 [%s] 暂无条目", category))
|
||||
}
|
||||
return c.Reply("📭 暂无条目")
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
if category != "" {
|
||||
sb.WriteString(fmt.Sprintf("📋 分类 [%s] 的条目:\n\n", html.EscapeString(category)))
|
||||
} else {
|
||||
sb.WriteString("📋 所有条目:\n\n")
|
||||
}
|
||||
|
||||
currentCat := ""
|
||||
for _, e := range entries {
|
||||
if category == "" && e.Category != currentCat {
|
||||
currentCat = e.Category
|
||||
sb.WriteString(fmt.Sprintf("\n【%s】\n", html.EscapeString(currentCat)))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("• <code>%s</code> %s\n", e.ID, html.EscapeString(e.Title)))
|
||||
}
|
||||
|
||||
return c.Reply(sb.String(), tele.ModeHTML)
|
||||
}
|
||||
|
||||
func (b *Bot) handleRefresh(c tele.Context) error {
|
||||
b.toc.TriggerUpdate()
|
||||
return c.Reply("🔄 目录刷新已触发")
|
||||
}
|
||||
|
||||
// handleDeleteEntryCallback 处理删除条目的回调
|
||||
func (b *Bot) handleDeleteEntryCallback(c tele.Context, entryID string, deleteChannelMsg bool) error {
|
||||
entry, err := b.storage.GetEntry(entryID)
|
||||
if err != nil {
|
||||
c.Edit(fmt.Sprintf("❌ %v", err))
|
||||
return c.Respond(&tele.CallbackResponse{Text: "条目不存在"})
|
||||
}
|
||||
|
||||
// 根据用户选择决定是否删除频道消息
|
||||
msgDeleted := false
|
||||
if deleteChannelMsg {
|
||||
if msgID := parseMessageIDFromLink(entry.Link); msgID != 0 {
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tele "gopkg.in/telebot.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
cbPrefixDelEntry = "del:"
|
||||
cbPrefixDelWithMsg = "delwithmsg:"
|
||||
cbPrefixDelOnlyToc = "delonlytoc:"
|
||||
)
|
||||
|
||||
func (b *Bot) handleEntryDel(c tele.Context) error {
|
||||
id := strings.TrimSpace(c.Message().Payload)
|
||||
if id == "" {
|
||||
return c.Reply("用法: /del <ID>")
|
||||
}
|
||||
|
||||
entry, err := b.storage.GetEntry(id)
|
||||
if err != nil {
|
||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||
}
|
||||
|
||||
menu := &tele.ReplyMarkup{}
|
||||
delWithMsgBtn := menu.Data("🗑 删除条目+频道消息", cbPrefixDelWithMsg+id)
|
||||
delOnlyTocBtn := menu.Data("📝 仅删除目录条目", cbPrefixDelOnlyToc+id)
|
||||
cancelBtn := menu.Data("❌ 取消", cbCancel)
|
||||
menu.Inline(
|
||||
menu.Row(delWithMsgBtn),
|
||||
menu.Row(delOnlyTocBtn),
|
||||
menu.Row(cancelBtn),
|
||||
)
|
||||
|
||||
text := fmt.Sprintf("🗑 确认删除?\n\n分类: %s\n标题: %s\n\n请选择删除方式:", entry.Category, entry.Title)
|
||||
return c.Reply(text, menu)
|
||||
}
|
||||
|
||||
func (b *Bot) handleEntryEdit(c tele.Context) error {
|
||||
args := strings.SplitN(c.Message().Payload, " ", 2)
|
||||
if len(args) < 2 {
|
||||
return c.Reply("用法: /edit <ID> <新标题>")
|
||||
}
|
||||
|
||||
id := args[0]
|
||||
newTitle := strings.TrimSpace(args[1])
|
||||
|
||||
if err := b.storage.UpdateEntryTitle(id, newTitle); err != nil {
|
||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||
}
|
||||
|
||||
b.toc.TriggerUpdate()
|
||||
return c.Reply(fmt.Sprintf("✅ 标题已更新为: %s", newTitle))
|
||||
}
|
||||
|
||||
func (b *Bot) handleEntryMove(c tele.Context) error {
|
||||
args := strings.Fields(c.Message().Payload)
|
||||
if len(args) < 2 {
|
||||
return c.Reply("用法: /move <ID> <新分类>")
|
||||
}
|
||||
|
||||
id := args[0]
|
||||
newCategory := args[1]
|
||||
|
||||
if !b.storage.CategoryExists(newCategory) {
|
||||
return c.Reply(fmt.Sprintf("❌ 分类 [%s] 不存在", newCategory))
|
||||
}
|
||||
|
||||
if err := b.storage.UpdateEntryCategory(id, newCategory); err != nil {
|
||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||
}
|
||||
|
||||
b.toc.TriggerUpdate()
|
||||
return c.Reply(fmt.Sprintf("✅ 已移动到分类: %s", newCategory))
|
||||
}
|
||||
|
||||
func (b *Bot) handleEntryList(c tele.Context) error {
|
||||
category := strings.TrimSpace(c.Message().Payload)
|
||||
|
||||
entries, err := b.storage.ListEntries(category)
|
||||
if err != nil {
|
||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
if category != "" {
|
||||
return c.Reply(fmt.Sprintf("📭 分类 [%s] 暂无条目", category))
|
||||
}
|
||||
return c.Reply("📭 暂无条目")
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
if category != "" {
|
||||
sb.WriteString(fmt.Sprintf("📋 分类 [%s] 的条目:\n\n", html.EscapeString(category)))
|
||||
} else {
|
||||
sb.WriteString("📋 所有条目:\n\n")
|
||||
}
|
||||
|
||||
currentCat := ""
|
||||
for _, e := range entries {
|
||||
if category == "" && e.Category != currentCat {
|
||||
currentCat = e.Category
|
||||
sb.WriteString(fmt.Sprintf("\n【%s】\n", html.EscapeString(currentCat)))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("• <code>%s</code> %s\n", e.ID, html.EscapeString(e.Title)))
|
||||
}
|
||||
|
||||
return c.Reply(sb.String(), tele.ModeHTML)
|
||||
}
|
||||
|
||||
func (b *Bot) handleRefresh(c tele.Context) error {
|
||||
b.toc.TriggerUpdate()
|
||||
return c.Reply("🔄 目录刷新已触发")
|
||||
}
|
||||
|
||||
// handleDeleteEntryCallback 处理删除条目的回调
|
||||
func (b *Bot) handleDeleteEntryCallback(c tele.Context, entryID string, deleteChannelMsg bool) error {
|
||||
entry, err := b.storage.GetEntry(entryID)
|
||||
if err != nil {
|
||||
c.Edit(fmt.Sprintf("❌ %v", err))
|
||||
return c.Respond(&tele.CallbackResponse{Text: "条目不存在"})
|
||||
}
|
||||
|
||||
// 根据用户选择决定是否删除频道消息
|
||||
msgDeleted := false
|
||||
if deleteChannelMsg {
|
||||
if msgID := parseMessageIDFromLink(entry.MessageLink()); msgID != 0 {
|
||||
channel := &tele.Chat{ID: b.cfg.Channel.ID}
|
||||
msg := &tele.Message{ID: msgID, Chat: channel}
|
||||
if err := b.bot.Delete(msg); err == nil {
|
||||
msgDeleted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.storage.DeleteEntry(entryID); err != nil {
|
||||
c.Edit(fmt.Sprintf("❌ 删除失败: %v", err))
|
||||
return c.Respond(&tele.CallbackResponse{Text: "删除失败"})
|
||||
}
|
||||
|
||||
b.toc.TriggerUpdate()
|
||||
|
||||
var text string
|
||||
if deleteChannelMsg && msgDeleted {
|
||||
text = fmt.Sprintf("✅ 已删除: [%s] %s\n📨 频道消息已同步删除", entry.Category, entry.Title)
|
||||
} else if deleteChannelMsg && !msgDeleted {
|
||||
text = fmt.Sprintf("✅ 已删除: [%s] %s\n⚠️ 频道消息删除失败(可能已被删除)", entry.Category, entry.Title)
|
||||
} else {
|
||||
text = fmt.Sprintf("✅ 已删除: [%s] %s\n📝 频道消息已保留", entry.Category, entry.Title)
|
||||
}
|
||||
|
||||
c.Edit(text)
|
||||
return c.Respond(&tele.CallbackResponse{Text: "删除成功"})
|
||||
}
|
||||
|
||||
// parseMessageIDFromLink 从链接中解析消息ID
|
||||
// 支持格式: https://t.me/c/123456/789 或 https://t.me/username/789
|
||||
func parseMessageIDFromLink(link string) int {
|
||||
parts := strings.Split(link, "/")
|
||||
if len(parts) < 1 {
|
||||
return 0
|
||||
}
|
||||
msgID, err := strconv.Atoi(parts[len(parts)-1])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return msgID
|
||||
}
|
||||
msgDeleted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.storage.DeleteEntry(entryID); err != nil {
|
||||
c.Edit(fmt.Sprintf("❌ 删除失败: %v", err))
|
||||
return c.Respond(&tele.CallbackResponse{Text: "删除失败"})
|
||||
}
|
||||
|
||||
b.toc.TriggerUpdate()
|
||||
|
||||
var text string
|
||||
if deleteChannelMsg && msgDeleted {
|
||||
text = fmt.Sprintf("✅ 已删除: [%s] %s\n📨 频道消息已同步删除", entry.Category, entry.Title)
|
||||
} else if deleteChannelMsg && !msgDeleted {
|
||||
text = fmt.Sprintf("✅ 已删除: [%s] %s\n⚠️ 频道消息删除失败(可能已被删除)", entry.Category, entry.Title)
|
||||
} else {
|
||||
text = fmt.Sprintf("✅ 已删除: [%s] %s\n📝 频道消息已保留", entry.Category, entry.Title)
|
||||
}
|
||||
|
||||
c.Edit(text)
|
||||
return c.Respond(&tele.CallbackResponse{Text: "删除成功"})
|
||||
}
|
||||
|
||||
// parseMessageIDFromLink 从链接中解析消息ID
|
||||
// 支持格式: https://t.me/c/123456/789 或 https://t.me/username/789
|
||||
func parseMessageIDFromLink(link string) int {
|
||||
parts := strings.Split(link, "/")
|
||||
if len(parts) < 1 {
|
||||
return 0
|
||||
}
|
||||
msgID, err := strconv.Atoi(parts[len(parts)-1])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return msgID
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tgchanbot/internal/storage"
|
||||
|
||||
@@ -21,10 +22,12 @@ const (
|
||||
)
|
||||
|
||||
// 相册收集定时器
|
||||
var (
|
||||
albumTimers = make(map[int64]*time.Timer)
|
||||
albumTimersMu sync.Mutex
|
||||
)
|
||||
var (
|
||||
albumTimers = make(map[int64]*time.Timer)
|
||||
albumTimersMu sync.Mutex
|
||||
)
|
||||
|
||||
var directURLPattern = regexp.MustCompile(`https?://[^\s<>"']+`)
|
||||
|
||||
func (b *Bot) handlePost(c tele.Context) error {
|
||||
msg := c.Message()
|
||||
@@ -69,20 +72,20 @@ func (b *Bot) handleQuickPost(c tele.Context, replyMsg *tele.Message, payload st
|
||||
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))
|
||||
}
|
||||
channelLink := buildMessageLinkFromReply(c.Message(), replyMsg)
|
||||
directLink := extractDirectLink(replyMsg)
|
||||
|
||||
// 创建条目
|
||||
entry, err := b.storage.CreateEntry(category, title, channelLink, directLink)
|
||||
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)
|
||||
}
|
||||
return c.Reply(fmt.Sprintf("✅ 已添加\n\nID: `%s`\n分类: %s\n标题: %s\n链接: %s",
|
||||
entry.ID, entry.Category, entry.Title, entry.MessageLink()), tele.ModeMarkdown)
|
||||
}
|
||||
|
||||
func buildMessageLinkFromReply(currentMsg, replyMsg *tele.Message) string {
|
||||
chat := currentMsg.Chat
|
||||
@@ -437,13 +440,13 @@ func (b *Bot) handleConfirmCallback(c tele.Context, userID int64, action string)
|
||||
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: "保存失败"})
|
||||
channelLink := b.buildChannelLink(channelMsg.ID)
|
||||
directLink := extractDirectLinkFromState(state)
|
||||
|
||||
entry, err := b.storage.CreateEntry(state.SelectedCat, title, channelLink, directLink)
|
||||
if err != nil {
|
||||
c.Edit(fmt.Sprintf("❌ 保存失败: %v", err))
|
||||
return c.Respond(&tele.CallbackResponse{Text: "保存失败"})
|
||||
}
|
||||
|
||||
b.toc.TriggerUpdate()
|
||||
@@ -499,7 +502,7 @@ func (b *Bot) buildChannelLink(msgID int) string {
|
||||
return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID)
|
||||
}
|
||||
|
||||
func extractTitle(msg *tele.Message) string {
|
||||
func extractTitle(msg *tele.Message) string {
|
||||
text := msg.Text
|
||||
if text == "" {
|
||||
text = msg.Caption
|
||||
@@ -516,8 +519,58 @@ func extractTitle(msg *tele.Message) string {
|
||||
title = "无标题"
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func extractDirectLinkFromMessages(msgs []*tele.Message) string {
|
||||
for _, msg := range msgs {
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
if link := extractDirectLink(msg); link != "" {
|
||||
return link
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractDirectLinkFromState(state *PostState) string {
|
||||
if state == nil {
|
||||
return ""
|
||||
}
|
||||
if link := extractDirectLink(state.ForwardedMsg); link != "" {
|
||||
return link
|
||||
}
|
||||
return extractDirectLinkFromMessages(state.ForwardedMsgs)
|
||||
}
|
||||
|
||||
func extractDirectLink(msg *tele.Message) string {
|
||||
if msg == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, entity := range msg.Entities {
|
||||
if entity.Type == tele.EntityTextLink && entity.URL != "" {
|
||||
return entity.URL
|
||||
}
|
||||
}
|
||||
for _, entity := range msg.CaptionEntities {
|
||||
if entity.Type == tele.EntityTextLink && entity.URL != "" {
|
||||
return entity.URL
|
||||
}
|
||||
}
|
||||
|
||||
for _, text := range []string{msg.Text, msg.Caption} {
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
if match := directURLPattern.FindString(text); match != "" {
|
||||
return match
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func buildMessageLink(msg *tele.Message) string {
|
||||
if msg.OriginalChat == nil {
|
||||
|
||||
Reference in New Issue
Block a user