diff --git a/internal/storage/entry.go b/internal/storage/entry.go index 1140e7a..e1bdd8d 100644 --- a/internal/storage/entry.go +++ b/internal/storage/entry.go @@ -9,13 +9,15 @@ import ( bolt "go.etcd.io/bbolt" ) -func (s *Storage) CreateEntry(category, title, link string) (*Entry, error) { +func (s *Storage) CreateEntry(category, title, channelLink, directLink string) (*Entry, error) { entry := &Entry{ - ID: shortid.MustGenerate(), - Category: category, - Title: title, - Link: link, - Timestamp: time.Now(), + ID: shortid.MustGenerate(), + Category: category, + Title: title, + Link: channelLink, + ChannelLink: channelLink, + DirectLink: directLink, + Timestamp: time.Now(), } err := s.db.Update(func(tx *bolt.Tx) error { diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 0142a48..0e1b8ae 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" bolt "go.etcd.io/bbolt" @@ -27,11 +28,25 @@ type Category struct { } type Entry struct { - ID string `json:"id"` - Category string `json:"category"` - Title string `json:"title"` - Link string `json:"link"` - Timestamp time.Time `json:"timestamp"` + ID string `json:"id"` + Category string `json:"category"` + Title string `json:"title"` + Link string `json:"link,omitempty"` // 兼容旧数据 + ChannelLink string `json:"channel_link,omitempty"` + DirectLink string `json:"direct_link,omitempty"` + Timestamp time.Time `json:"timestamp"` +} + +func (e Entry) MessageLink() string { + if e.ChannelLink != "" { + return e.ChannelLink + } + return e.Link +} + +func (e Entry) HasDirectLink() bool { + direct := strings.TrimSpace(e.DirectLink) + return direct != "" && direct != e.MessageLink() } type Storage struct { diff --git a/internal/telegram/handlers_entry.go b/internal/telegram/handlers_entry.go index 7fdbda7..518107c 100644 --- a/internal/telegram/handlers_entry.go +++ b/internal/telegram/handlers_entry.go @@ -128,7 +128,7 @@ func (b *Bot) handleDeleteEntryCallback(c tele.Context, entryID string, deleteCh // 根据用户选择决定是否删除频道消息 msgDeleted := false if deleteChannelMsg { - if msgID := parseMessageIDFromLink(entry.Link); msgID != 0 { + 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 { diff --git a/internal/telegram/handlers_post.go b/internal/telegram/handlers_post.go index 77adf73..a337417 100644 --- a/internal/telegram/handlers_post.go +++ b/internal/telegram/handlers_post.go @@ -2,6 +2,7 @@ package telegram import ( "fmt" + "regexp" "strings" "sync" "time" @@ -26,6 +27,8 @@ var ( albumTimersMu sync.Mutex ) +var directURLPattern = regexp.MustCompile(`https?://[^\s<>"']+`) + func (b *Bot) handlePost(c tele.Context) error { msg := c.Message() payload := strings.TrimSpace(c.Message().Payload) @@ -69,11 +72,11 @@ func (b *Bot) handleQuickPost(c tele.Context, replyMsg *tele.Message, payload st title = extractTitle(replyMsg) } - // 构建链接 - link := buildMessageLinkFromReply(c.Message(), replyMsg) + channelLink := buildMessageLinkFromReply(c.Message(), replyMsg) + directLink := extractDirectLink(replyMsg) // 创建条目 - entry, err := b.storage.CreateEntry(category, title, link) + entry, err := b.storage.CreateEntry(category, title, channelLink, directLink) if err != nil { return c.Reply(fmt.Sprintf("❌ 保存失败: %v", err)) } @@ -81,7 +84,7 @@ func (b *Bot) handleQuickPost(c tele.Context, replyMsg *tele.Message, payload st 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) + entry.ID, entry.Category, entry.Title, entry.MessageLink()), tele.ModeMarkdown) } func buildMessageLinkFromReply(currentMsg, replyMsg *tele.Message) string { @@ -116,7 +119,7 @@ func (b *Bot) handleTextInput(c tele.Context) error { state := b.states.Get(c.Sender().ID) // 私聊收到转发消息,直接启动投稿流程(无需先 /post) - if isForwarded && b.cfg.IsAdmin(c.Sender().ID) { + if isForwarded && b.isAdmin(c.Sender().ID) { if state == nil { b.states.StartPost(c.Sender().ID) } @@ -138,7 +141,7 @@ func (b *Bot) handleTextInput(c tele.Context) error { } func (b *Bot) handleForwarded(c tele.Context) error { - if !b.cfg.IsAdmin(c.Sender().ID) { + if !b.isAdmin(c.Sender().ID) { return nil } @@ -270,7 +273,7 @@ func (b *Bot) buildCategoryKeyboard(categories []storage.Category) *tele.ReplyMa } func (b *Bot) handleCallback(c tele.Context) error { - if !b.cfg.IsAdmin(c.Sender().ID) { + if !b.isAdmin(c.Sender().ID) { return c.Respond(&tele.CallbackResponse{Text: "无权限"}) } @@ -437,10 +440,10 @@ func (b *Bot) handleConfirmCallback(c tele.Context, userID int64, action string) return c.Respond(&tele.CallbackResponse{Text: "发送失败"}) } - // 使用频道消息的链接 - link := b.buildChannelLink(channelMsg.ID) + channelLink := b.buildChannelLink(channelMsg.ID) + directLink := extractDirectLinkFromState(state) - entry, err := b.storage.CreateEntry(state.SelectedCat, title, link) + 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: "保存失败"}) @@ -519,6 +522,56 @@ func extractTitle(msg *tele.Message) string { 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 { return "" diff --git a/internal/toc/renderer.go b/internal/toc/renderer.go index 62a374f..204f8e7 100644 --- a/internal/toc/renderer.go +++ b/internal/toc/renderer.go @@ -39,7 +39,11 @@ func (m *Manager) Render() (string, error) { sb.WriteString(" 暂无内容\n") } else { for _, entry := range entries { - sb.WriteString(fmt.Sprintf(" • %s\n", entry.Link, html.EscapeString(entry.Title))) + sb.WriteString(fmt.Sprintf(" • %s", html.EscapeString(entry.MessageLink()), html.EscapeString(entry.Title))) + if entry.HasDirectLink() { + sb.WriteString(fmt.Sprintf(" [直达]", html.EscapeString(entry.DirectLink))) + } + sb.WriteString("\n") } } sb.WriteString("\n")