Compare commits
2 Commits
8a6859269c
...
3fea0ee89c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fea0ee89c | ||
|
|
e5a717e94e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
config.yaml
|
config.yaml
|
||||||
/data
|
/data
|
||||||
bot
|
bot
|
||||||
|
mygo_chanbot
|
||||||
21
README.md
21
README.md
@@ -1,5 +1,7 @@
|
|||||||
# Telegram 频道目录机器人
|
# Telegram 频道目录机器人
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
交互式、数据库驱动的 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
BIN
assets/mygo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
@@ -28,7 +28,8 @@ 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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
storage *storage.Storage
|
storage *storage.Storage
|
||||||
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,13 +24,14 @@ 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,
|
||||||
stopCh: make(chan struct{}),
|
coverImage: coverImage,
|
||||||
|
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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user