diff --git a/README.md b/README.md index d302739..b6de633 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Telegram 频道目录机器人 +![Cover](assets/mygo.png) + 交互式、数据库驱动的 Telegram 频道内容管理系统。 ## 功能 @@ -48,18 +50,33 @@ toc: ## 命令 +### 投稿管理 + | 命令 | 说明 | |------|------| | `/post` | 开始投稿流程 | | `/post <分类> /tt <标题>` | 快捷投稿 (回复消息时使用) | | `/list [分类]` | 查看条目 | -| `/del ` | 删除条目 | +| `/del ` | 删除条目(同时删除频道消息) | | `/edit <新标题>` | 修改标题 | | `/move <新分类>` | 移动条目到其他分类 | +| `/refresh` | 手动刷新频道目录 | + +### 分类管理 + +| 命令 | 说明 | +|------|------| | `/cat_add <名称> [排序]` | 创建分类 | | `/cat_del <名称>` | 删除分类 | | `/cat_list` | 列出所有分类 | -| `/refresh` | 手动刷新频道目录 | + +### 管理员管理 (仅超级管理员) + +| 命令 | 说明 | +|------|------| +| `/admin_add <用户ID>` | 添加管理员 | +| `/admin_del <用户ID>` | 移除管理员 | +| `/admin_list` | 列出所有管理员 | ## 投稿方式 diff --git a/assets/mygo.png b/assets/mygo.png new file mode 100644 index 0000000..6f98f7d Binary files /dev/null and b/assets/mygo.png differ diff --git a/internal/config/config.go b/internal/config/config.go index d1c99dd..58111dc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,7 +28,8 @@ type DatabaseConfig struct { } type TOCConfig struct { - DebounceSeconds int `yaml:"debounce_seconds"` + DebounceSeconds int `yaml:"debounce_seconds"` + CoverImage string `yaml:"cover_image"` // 封面图片路径,留空则不使用图片 } func Load(path string) (*Config, error) { diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 5992e09..126020d 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -37,7 +37,7 @@ func New(cfg *config.Config, store *storage.Storage) (*Bot, error) { states: NewStateManager(), } - bot.toc = toc.NewManager(store, b, cfg.Channel.ID, time.Duration(cfg.TOC.DebounceSeconds)*time.Second) + bot.toc = toc.NewManager(store, b, cfg.Channel.ID, time.Duration(cfg.TOC.DebounceSeconds)*time.Second, cfg.TOC.CoverImage) bot.setupRoutes() diff --git a/internal/telegram/handlers_entry.go b/internal/telegram/handlers_entry.go index 31c2b8a..82f8a1f 100644 --- a/internal/telegram/handlers_entry.go +++ b/internal/telegram/handlers_entry.go @@ -2,6 +2,7 @@ package telegram import ( "fmt" + "strconv" "strings" tele "gopkg.in/telebot.v3" @@ -18,11 +19,25 @@ func (b *Bot) handleEntryDel(c tele.Context) error { return c.Reply(fmt.Sprintf("❌ %v", err)) } + // 尝试删除频道消息 + msgDeleted := false + if msgID := parseMessageIDFromLink(entry.Link); 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(id); err != nil { return c.Reply(fmt.Sprintf("❌ 删除失败: %v", err)) } b.toc.TriggerUpdate() + + if msgDeleted { + return c.Reply(fmt.Sprintf("✅ 已删除: [%s] %s\n📨 频道消息已同步删除", entry.Category, entry.Title)) + } return c.Reply(fmt.Sprintf("✅ 已删除: [%s] %s", entry.Category, entry.Title)) } @@ -102,3 +117,17 @@ func (b *Bot) handleRefresh(c tele.Context) error { b.toc.TriggerUpdate() return c.Reply("🔄 目录刷新已触发") } + +// 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 +} diff --git a/internal/toc/manager.go b/internal/toc/manager.go index 18b2688..29bcdea 100644 --- a/internal/toc/manager.go +++ b/internal/toc/manager.go @@ -12,10 +12,11 @@ import ( ) type Manager struct { - storage *storage.Storage - bot *tele.Bot - chanID int64 - debounce time.Duration + storage *storage.Storage + bot *tele.Bot + chanID int64 + debounce time.Duration + coverImage string mu sync.Mutex pending bool @@ -23,13 +24,14 @@ type Manager struct { stopCh chan struct{} } -func NewManager(store *storage.Storage, bot *tele.Bot, chanID int64, debounce time.Duration) *Manager { +func NewManager(store *storage.Storage, bot *tele.Bot, chanID int64, debounce time.Duration, coverImage string) *Manager { return &Manager{ - storage: store, - bot: bot, - chanID: chanID, - debounce: debounce, - stopCh: make(chan struct{}), + storage: store, + bot: bot, + chanID: chanID, + debounce: debounce, + coverImage: coverImage, + stopCh: make(chan struct{}), } } @@ -72,6 +74,12 @@ func (m *Manager) updateMessage(content string) error { return err } + // 带封面图片模式 + if m.coverImage != "" { + return m.updateWithPhoto(chat, msgID, content) + } + + // 纯文本模式 if msgID == 0 { msg, err := m.bot.Send(chat, content, tele.ModeMarkdown, tele.NoPreview) if err != nil { @@ -105,6 +113,47 @@ func (m *Manager) updateMessage(content string) error { return nil } +func (m *Manager) updateWithPhoto(chat *tele.Chat, msgID int, content string) error { + photo := &tele.Photo{ + File: tele.FromDisk(m.coverImage), + Caption: content, + } + + if msgID == 0 { + msg, err := m.bot.Send(chat, photo, tele.ModeMarkdown) + if err != nil { + return err + } + return m.storage.SetTocMsgID(msg.ID) + } + + existingMsg := &tele.Message{ + ID: msgID, + Chat: chat, + } + + // 编辑图片消息的 caption + _, err := m.bot.EditCaption(existingMsg, content, tele.ModeMarkdown) + if err != nil { + errMsg := err.Error() + if err == tele.ErrMessageNotModified || strings.Contains(errMsg, "message is not modified") { + return nil + } + // 旧消息不是图片或找不到,重新发送 + if strings.Contains(errMsg, "message to edit not found") || + strings.Contains(errMsg, "no caption") { + msg, err := m.bot.Send(chat, photo, tele.ModeMarkdown) + if err != nil { + return err + } + return m.storage.SetTocMsgID(msg.ID) + } + return err + } + + return nil +} + func (m *Manager) Stop() { m.mu.Lock() defer m.mu.Unlock() diff --git a/internal/toc/renderer.go b/internal/toc/renderer.go index 1bdfd6d..b7afdc4 100644 --- a/internal/toc/renderer.go +++ b/internal/toc/renderer.go @@ -45,7 +45,6 @@ func (m *Manager) Render() (string, error) { } sb.WriteString("━━━━━━━━━━━━━━━\n") - sb.WriteString("_自动生成_") return sb.String(), nil }