Compare commits

..

2 Commits

Author SHA1 Message Date
dela
3fea0ee89c 更新封面和readme 2026-02-05 10:17:55 +08:00
dela
e5a717e94e 修复发图片/文件/视频等消息时不会触发 2026-02-05 09:48:32 +08:00
8 changed files with 119 additions and 16 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
config.yaml config.yaml
/data /data
bot bot
mygo_chanbot

View File

@@ -1,5 +1,7 @@
# Telegram 频道目录机器人 # Telegram 频道目录机器人
![Cover](assets/mygo.png)
交互式、数据库驱动的 Telegram 频道内容管理系统。 交互式、数据库驱动的 Telegram 频道内容管理系统。
## 功能 ## 功能
@@ -48,18 +50,33 @@ toc:
## 命令 ## 命令
### 投稿管理
| 命令 | 说明 | | 命令 | 说明 |
|------|------| |------|------|
| `/post` | 开始投稿流程 | | `/post` | 开始投稿流程 |
| `/post <分类> /tt <标题>` | 快捷投稿 (回复消息时使用) | | `/post <分类> /tt <标题>` | 快捷投稿 (回复消息时使用) |
| `/list [分类]` | 查看条目 | | `/list [分类]` | 查看条目 |
| `/del <ID>` | 删除条目 | | `/del <ID>` | 删除条目(同时删除频道消息) |
| `/edit <ID> <新标题>` | 修改标题 | | `/edit <ID> <新标题>` | 修改标题 |
| `/move <ID> <新分类>` | 移动条目到其他分类 | | `/move <ID> <新分类>` | 移动条目到其他分类 |
| `/refresh` | 手动刷新频道目录 |
### 分类管理
| 命令 | 说明 |
|------|------|
| `/cat_add <名称> [排序]` | 创建分类 | | `/cat_add <名称> [排序]` | 创建分类 |
| `/cat_del <名称>` | 删除分类 | | `/cat_del <名称>` | 删除分类 |
| `/cat_list` | 列出所有分类 | | `/cat_list` | 列出所有分类 |
| `/refresh` | 手动刷新频道目录 |
### 管理员管理 (仅超级管理员)
| 命令 | 说明 |
|------|------|
| `/admin_add <用户ID>` | 添加管理员 |
| `/admin_del <用户ID>` | 移除管理员 |
| `/admin_list` | 列出所有管理员 |
## 投稿方式 ## 投稿方式

BIN
assets/mygo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@@ -29,6 +29,7 @@ type DatabaseConfig struct {
type TOCConfig 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) { func Load(path string) (*Config, error) {

View File

@@ -37,7 +37,7 @@ func New(cfg *config.Config, store *storage.Storage) (*Bot, error) {
states: NewStateManager(), 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() bot.setupRoutes()
@@ -56,6 +56,13 @@ func (b *Bot) setupRoutes() {
// Post flow // Post flow
adminOnly.Handle("/post", b.handlePost) adminOnly.Handle("/post", b.handlePost)
adminOnly.Handle(tele.OnText, b.handleTextInput) adminOnly.Handle(tele.OnText, b.handleTextInput)
adminOnly.Handle(tele.OnPhoto, b.handleTextInput)
adminOnly.Handle(tele.OnVideo, b.handleTextInput)
adminOnly.Handle(tele.OnDocument, b.handleTextInput)
adminOnly.Handle(tele.OnAudio, b.handleTextInput)
adminOnly.Handle(tele.OnVoice, b.handleTextInput)
adminOnly.Handle(tele.OnAnimation, b.handleTextInput)
adminOnly.Handle(tele.OnSticker, b.handleTextInput)
// Entry management // Entry management
adminOnly.Handle("/del", b.handleEntryDel) adminOnly.Handle("/del", b.handleEntryDel)

View File

@@ -2,6 +2,7 @@ package telegram
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
tele "gopkg.in/telebot.v3" tele "gopkg.in/telebot.v3"
@@ -18,11 +19,25 @@ func (b *Bot) handleEntryDel(c tele.Context) error {
return c.Reply(fmt.Sprintf("❌ %v", err)) 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 { if err := b.storage.DeleteEntry(id); err != nil {
return c.Reply(fmt.Sprintf("❌ 删除失败: %v", err)) return c.Reply(fmt.Sprintf("❌ 删除失败: %v", err))
} }
b.toc.TriggerUpdate() 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)) 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() b.toc.TriggerUpdate()
return c.Reply("🔄 目录刷新已触发") 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
}

View File

@@ -16,6 +16,7 @@ type Manager struct {
bot *tele.Bot bot *tele.Bot
chanID int64 chanID int64
debounce time.Duration debounce time.Duration
coverImage string
mu sync.Mutex mu sync.Mutex
pending bool pending bool
@@ -23,12 +24,13 @@ type Manager struct {
stopCh chan 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{ return &Manager{
storage: store, storage: store,
bot: bot, bot: bot,
chanID: chanID, chanID: chanID,
debounce: debounce, debounce: debounce,
coverImage: coverImage,
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
} }
} }
@@ -72,6 +74,12 @@ func (m *Manager) updateMessage(content string) error {
return err return err
} }
// 带封面图片模式
if m.coverImage != "" {
return m.updateWithPhoto(chat, msgID, content)
}
// 纯文本模式
if msgID == 0 { if msgID == 0 {
msg, err := m.bot.Send(chat, content, tele.ModeMarkdown, tele.NoPreview) msg, err := m.bot.Send(chat, content, tele.ModeMarkdown, tele.NoPreview)
if err != nil { if err != nil {
@@ -105,6 +113,47 @@ func (m *Manager) updateMessage(content string) error {
return nil 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() { func (m *Manager) Stop() {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()

View File

@@ -45,7 +45,6 @@ func (m *Manager) Render() (string, error) {
} }
sb.WriteString("━━━━━━━━━━━━━━━\n") sb.WriteString("━━━━━━━━━━━━━━━\n")
sb.WriteString("_自动生成_")
return sb.String(), nil return sb.String(), nil
} }