Refactor post handling and add command setup for Telegram bot

This commit is contained in:
dela
2026-02-05 00:52:29 +08:00
parent d82badc6e3
commit 8a6859269c
9 changed files with 612 additions and 21 deletions

132
README.md Normal file
View File

@@ -0,0 +1,132 @@
# Telegram 频道目录机器人
交互式、数据库驱动的 Telegram 频道内容管理系统。
## 功能
- **数据库管理分类** - 动态 CRUD无需硬编码
- **交互式按钮投稿** - Inline Keyboard 选择分类
- **快捷回复投稿** - 回复消息直接添加到目录
- **内容同步发布** - 转发的内容会复制发送到频道(非转发,可编辑)
- **防抖目录更新** - 多次操作合并为一次渲染,避免限流
## 安装
```bash
go mod tidy
go build -o bot ./cmd/bot
```
## 配置
编辑 `config.yaml`:
```yaml
bot:
token: "YOUR_BOT_TOKEN"
admins:
- 123456789 # 管理员 Telegram User ID
channel:
id: -1001234567890 # 目标频道 ID
database:
path: "./data/bot.db"
toc:
debounce_seconds: 3
```
## 运行
```bash
./bot
# 或指定配置文件
./bot -config /path/to/config.yaml
```
## 命令
| 命令 | 说明 |
|------|------|
| `/post` | 开始投稿流程 |
| `/post <分类> /tt <标题>` | 快捷投稿 (回复消息时使用) |
| `/list [分类]` | 查看条目 |
| `/del <ID>` | 删除条目 |
| `/edit <ID> <新标题>` | 修改标题 |
| `/move <ID> <新分类>` | 移动条目到其他分类 |
| `/cat_add <名称> [排序]` | 创建分类 |
| `/cat_del <名称>` | 删除分类 |
| `/cat_list` | 列出所有分类 |
| `/refresh` | 手动刷新频道目录 |
## 投稿方式
### 方式一:交互式流程(私聊 Bot
```
1. 私聊 Bot 发送 /post
2. 转发任意消息给 Bot支持来自任意来源个人、群组、频道等
3. 点击分类按钮
4. 确认标题 (使用默认 / 自定义输入)
5. 确认添加
```
Bot 会将消息内容复制发送到频道,目录链接指向频道中的新消息。
### 方式二:快捷回复(群组内)
```
1. 在关联群组找到目标消息
2. 回复该消息,发送:
/post iOS /tt 某个APP推荐
```
- `/tt` 后的内容为自定义标题
- 省略标题则自动提取消息首行
## 支持的消息类型
- ✅ 纯文本
- ✅ 图片(带说明文字)
- ✅ 视频(带说明文字)
- ✅ 文档(带说明文字)
- ✅ 音频(带说明文字)
- ✅ 动图(带说明文字)
- ✅ 语音
- ✅ 贴纸
## 目录结构
```
├── cmd/bot/main.go # 程序入口
├── config.yaml # 配置文件
├── internal/
│ ├── config/config.go # 配置加载
│ ├── storage/
│ │ ├── storage.go # BoltDB 初始化
│ │ ├── category.go # 分类 CRUD
│ │ └── entry.go # 条目 CRUD
│ ├── telegram/
│ │ ├── bot.go # Bot 初始化与路由
│ │ ├── middleware.go # 权限中间件
│ │ ├── state.go # 用户状态管理
│ │ ├── handlers_cat.go # 分类命令
│ │ ├── handlers_post.go # 投稿流程
│ │ └── handlers_entry.go # 条目命令
│ └── toc/
│ ├── manager.go # 目录管理器 (防抖)
│ └── renderer.go # 目录渲染
```
## 技术栈
- Go 1.23+
- [telebot.v3](https://gopkg.in/telebot.v3) - Telegram Bot 框架
- [bbolt](https://go.etcd.io/bbolt) - 嵌入式 KV 数据库
- [shortid](https://github.com/teris-io/shortid) - 短 ID 生成
## License
MIT

View File

@@ -65,3 +65,13 @@ func (c *Config) IsAdmin(userID int64) bool {
}
return false
}
// IsSuperAdmin 检查是否为超级管理员(配置文件中的管理员)
func (c *Config) IsSuperAdmin(userID int64) bool {
for _, id := range c.Admins {
if id == userID {
return true
}
}
return false
}

View File

@@ -15,6 +15,7 @@ var (
bucketAppConfig = []byte("AppConfig")
bucketCategories = []byte("Categories")
bucketEntries = []byte("Entries")
bucketAdmins = []byte("Admins")
keyTocMsgID = []byte("toc_msg_id")
)
@@ -59,7 +60,7 @@ func New(path string) (*Storage, error) {
func (s *Storage) initBuckets() error {
return s.db.Update(func(tx *bolt.Tx) error {
buckets := [][]byte{bucketAppConfig, bucketCategories, bucketEntries}
buckets := [][]byte{bucketAppConfig, bucketCategories, bucketEntries, bucketAdmins}
for _, b := range buckets {
if _, err := tx.CreateBucketIfNotExists(b); err != nil {
return fmt.Errorf("failed to create bucket %s: %w", b, err)
@@ -102,3 +103,50 @@ func encodeJSON(v any) ([]byte, error) {
func decodeJSON(data []byte, v any) error {
return json.Unmarshal(data, v)
}
// Admin management
func (s *Storage) AddAdmin(userID int64) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketAdmins)
key := make([]byte, 8)
binary.BigEndian.PutUint64(key, uint64(userID))
return b.Put(key, []byte("1"))
})
}
func (s *Storage) RemoveAdmin(userID int64) error {
return s.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketAdmins)
key := make([]byte, 8)
binary.BigEndian.PutUint64(key, uint64(userID))
return b.Delete(key)
})
}
func (s *Storage) IsAdmin(userID int64) bool {
var exists bool
s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketAdmins)
key := make([]byte, 8)
binary.BigEndian.PutUint64(key, uint64(userID))
exists = b.Get(key) != nil
return nil
})
return exists
}
func (s *Storage) ListAdmins() ([]int64, error) {
var admins []int64
err := s.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketAdmins)
return b.ForEach(func(k, v []byte) error {
if len(k) == 8 {
userID := int64(binary.BigEndian.Uint64(k))
admins = append(admins, userID)
}
return nil
})
})
return admins, err
}

View File

@@ -55,7 +55,7 @@ func (b *Bot) setupRoutes() {
// Post flow
adminOnly.Handle("/post", b.handlePost)
adminOnly.Handle(tele.OnText, b.handleTextOrForwarded)
adminOnly.Handle(tele.OnText, b.handleTextInput)
// Entry management
adminOnly.Handle("/del", b.handleEntryDel)
@@ -66,15 +66,41 @@ func (b *Bot) setupRoutes() {
// TOC
adminOnly.Handle("/refresh", b.handleRefresh)
// Admin management (super admin only)
superAdminOnly := b.bot.Group()
superAdminOnly.Use(b.SuperAdminMiddleware())
superAdminOnly.Handle("/admin_add", b.handleAdminAdd)
superAdminOnly.Handle("/admin_del", b.handleAdminDel)
superAdminOnly.Handle("/admin_list", b.handleAdminList)
// Callbacks
b.bot.Handle(tele.OnCallback, b.handleCallback)
}
func (b *Bot) Start() {
b.setCommands()
log.Println("Bot started...")
b.bot.Start()
}
func (b *Bot) setCommands() {
commands := []tele.Command{
{Text: "post", Description: "投稿 - 添加频道内容到目录"},
{Text: "list", Description: "列表 - 查看所有条目"},
{Text: "cat_list", Description: "分类列表"},
{Text: "cat_add", Description: "添加分类 <名称> [排序]"},
{Text: "cat_del", Description: "删除分类 <名称>"},
{Text: "del", Description: "删除条目 <ID>"},
{Text: "edit", Description: "编辑标题 <ID> <新标题>"},
{Text: "move", Description: "移动条目 <ID> <新分类>"},
{Text: "refresh", Description: "刷新目录"},
}
if err := b.bot.SetCommands(commands); err != nil {
log.Printf("Failed to set commands: %v", err)
}
}
func (b *Bot) Stop() {
b.bot.Stop()
b.toc.Stop()

View File

@@ -0,0 +1,89 @@
package telegram
import (
"fmt"
"strconv"
"strings"
tele "gopkg.in/telebot.v3"
)
func (b *Bot) handleAdminAdd(c tele.Context) error {
payload := strings.TrimSpace(c.Message().Payload)
if payload == "" {
return c.Reply("用法: /admin_add <用户ID>")
}
userID, err := strconv.ParseInt(payload, 10, 64)
if err != nil {
return c.Reply("❌ 无效的用户ID")
}
// 检查是否已是管理员
if b.isAdmin(userID) {
return c.Reply("⚠️ 该用户已是管理员")
}
if err := b.storage.AddAdmin(userID); err != nil {
return c.Reply(fmt.Sprintf("❌ 添加失败: %v", err))
}
return c.Reply(fmt.Sprintf("✅ 已添加管理员: %d", userID))
}
func (b *Bot) handleAdminDel(c tele.Context) error {
payload := strings.TrimSpace(c.Message().Payload)
if payload == "" {
return c.Reply("用法: /admin_del <用户ID>")
}
userID, err := strconv.ParseInt(payload, 10, 64)
if err != nil {
return c.Reply("❌ 无效的用户ID")
}
// 不能删除超级管理员
if b.cfg.IsSuperAdmin(userID) {
return c.Reply("❌ 无法删除超级管理员(配置文件中的管理员)")
}
if err := b.storage.RemoveAdmin(userID); err != nil {
return c.Reply(fmt.Sprintf("❌ 删除失败: %v", err))
}
return c.Reply(fmt.Sprintf("✅ 已移除管理员: %d", userID))
}
func (b *Bot) handleAdminList(c tele.Context) error {
// 配置文件中的超级管理员
superAdmins := b.cfg.Admins
// 数据库中的动态管理员
dbAdmins, err := b.storage.ListAdmins()
if err != nil {
return c.Reply(fmt.Sprintf("❌ 获取管理员列表失败: %v", err))
}
var sb strings.Builder
sb.WriteString("👑 **管理员列表**\n\n")
sb.WriteString("**超级管理员(配置文件):**\n")
if len(superAdmins) == 0 {
sb.WriteString(" _无_\n")
} else {
for _, id := range superAdmins {
sb.WriteString(fmt.Sprintf(" • `%d`\n", id))
}
}
sb.WriteString("\n**动态管理员(数据库):**\n")
if len(dbAdmins) == 0 {
sb.WriteString(" _无_\n")
} else {
for _, id := range dbAdmins {
sb.WriteString(fmt.Sprintf(" • `%d`\n", id))
}
}
return c.Reply(sb.String(), tele.ModeMarkdown)
}

View File

@@ -12,20 +12,113 @@ import (
const (
cbPrefixCat = "cat:"
cbPrefixConfirm = "confirm:"
cbPrefixTitle = "title:"
cbCancel = "cancel"
)
func (b *Bot) handlePost(c tele.Context) error {
msg := c.Message()
payload := strings.TrimSpace(c.Message().Payload)
// 快捷方式: 回复消息 + /post <分类> /tt <标题>
if msg.ReplyTo != nil && payload != "" {
return b.handleQuickPost(c, msg.ReplyTo, payload)
}
// 常规交互流程
b.states.StartPost(c.Sender().ID)
return c.Reply("📨 请转发一条来自目标频道的消息")
return c.Reply("📨 请转发需要归档到目录的消息\n\n消息将被同步发送到频道并添加到目录\n\n💡 快捷方式: 回复消息并发送\n`/post <分类> /tt <标题>`", tele.ModeMarkdown)
}
func (b *Bot) handleTextOrForwarded(c tele.Context) error {
func (b *Bot) handleQuickPost(c tele.Context, replyMsg *tele.Message, payload string) error {
// 解析: <分类> /tt <标题>
parts := strings.SplitN(payload, "/tt", 2)
category := strings.TrimSpace(parts[0])
var title string
if len(parts) > 1 {
title = strings.TrimSpace(parts[1])
}
if category == "" {
return c.Reply("❌ 用法: /post <分类> /tt <标题>\n例如: /post iOS /tt 某个APP推荐")
}
// 验证分类存在
if !b.storage.CategoryExists(category) {
categories, _ := b.storage.ListCategories()
var names []string
for _, cat := range categories {
names = append(names, cat.Name)
}
return c.Reply(fmt.Sprintf("❌ 分类 [%s] 不存在\n\n可用分类: %s", category, strings.Join(names, ", ")))
}
// 标题: 优先使用指定标题,否则从回复消息提取
if title == "" {
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))
}
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)
}
func buildMessageLinkFromReply(currentMsg, replyMsg *tele.Message) string {
chat := currentMsg.Chat
msgID := replyMsg.ID
if chat.Username != "" {
return fmt.Sprintf("https://t.me/%s/%d", chat.Username, msgID)
}
chatID := chat.ID
if chatID < 0 {
chatID = -chatID - 1000000000000
}
return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID)
}
func (b *Bot) handleTextInput(c tele.Context) error {
msg := c.Message()
if msg == nil || msg.OriginalChat == nil {
if msg == nil {
return nil
}
// 只处理私聊消息,忽略群组对话
if c.Chat().Type != tele.ChatPrivate {
return nil
}
// 只处理有活跃投稿状态的用户
state := b.states.Get(c.Sender().ID)
if state == nil {
return nil
}
// 转发消息处理 (StepAwaitForward)
// OriginalChat: 转发自频道/群组; OriginalSender: 转发自个人用户
isForwarded := msg.OriginalChat != nil || msg.OriginalSender != nil
if isForwarded && state.Step == StepAwaitForward {
return b.handleForwarded(c)
}
// 自定义标题输入 (StepAwaitTitle)
if state.Step == StepAwaitTitle {
return b.handleTitleInput(c)
}
return nil
}
func (b *Bot) handleForwarded(c tele.Context) error {
@@ -39,8 +132,9 @@ func (b *Bot) handleForwarded(c tele.Context) error {
}
msg := c.Message()
if msg.OriginalChat == nil {
return c.Reply("❌ 这不是一条转发消息,请转发频道消息")
// 检查是否为转发消息(来自频道/群组或个人用户)
if msg.OriginalChat == nil && msg.OriginalSender == nil {
return c.Reply("❌ 这不是一条转发消息,请转发一条消息给我")
}
b.states.SetForwarded(c.Sender().ID, msg)
@@ -91,7 +185,8 @@ func (b *Bot) handleCallback(c tele.Context) error {
return c.Respond(&tele.CallbackResponse{Text: "无权限"})
}
data := c.Callback().Data
// telebot v3 会在 data 前加 \f 前缀,需要去掉
data := strings.TrimPrefix(c.Callback().Data, "\f")
userID := c.Sender().ID
switch {
@@ -102,6 +197,10 @@ func (b *Bot) handleCallback(c tele.Context) error {
category := strings.TrimPrefix(data, cbPrefixCat)
return b.handleCategoryCallback(c, userID, category)
case strings.HasPrefix(data, cbPrefixTitle):
action := strings.TrimPrefix(data, cbPrefixTitle)
return b.handleTitleCallback(c, userID, action)
case strings.HasPrefix(data, cbPrefixConfirm):
action := strings.TrimPrefix(data, cbPrefixConfirm)
return b.handleConfirmCallback(c, userID, action)
@@ -116,6 +215,74 @@ func (b *Bot) handleCancelCallback(c tele.Context, userID int64) error {
return c.Respond(&tele.CallbackResponse{Text: "已取消"})
}
func (b *Bot) handleTitleCallback(c tele.Context, userID int64, action string) error {
state := b.states.Get(userID)
if state == nil || state.Step != StepAwaitTitle {
return c.Respond(&tele.CallbackResponse{Text: "会话已过期"})
}
if action == "default" {
// 使用默认标题
title := extractTitle(state.ForwardedMsg)
b.states.SetTitle(userID, title)
return b.showConfirmation(c, state, title)
}
// 自定义标题 - 提示用户输入
c.Edit("✏️ 请发送新标题:")
return c.Respond()
}
func (b *Bot) handleTitleInput(c tele.Context) error {
state := b.states.Get(c.Sender().ID)
if state == nil || state.Step != StepAwaitTitle {
return nil
}
title := strings.TrimSpace(c.Message().Text)
if title == "" {
return c.Reply("❌ 标题不能为空,请重新输入:")
}
if len(title) > 50 {
title = title[:47] + "..."
}
b.states.SetTitle(c.Sender().ID, title)
return b.showConfirmationMsg(c, state, title)
}
func (b *Bot) showConfirmation(c tele.Context, state *PostState, title string) error {
channelName := "未知频道"
if state.ForwardedMsg.OriginalChat != nil {
channelName = state.ForwardedMsg.OriginalChat.Title
}
menu := &tele.ReplyMarkup{}
confirmBtn := menu.Data("✅ 确认添加", cbPrefixConfirm+"yes")
cancelBtn := menu.Data("❌ 取消", cbCancel)
menu.Inline(menu.Row(confirmBtn, cancelBtn))
text := fmt.Sprintf("📋 确认添加?\n\n频道: %s\n分类: %s\n标题: %s", channelName, state.SelectedCat, title)
c.Edit(text, menu)
return c.Respond()
}
func (b *Bot) showConfirmationMsg(c tele.Context, state *PostState, title string) error {
channelName := "未知频道"
if state.ForwardedMsg.OriginalChat != nil {
channelName = state.ForwardedMsg.OriginalChat.Title
}
menu := &tele.ReplyMarkup{}
confirmBtn := menu.Data("✅ 确认添加", cbPrefixConfirm+"yes")
cancelBtn := menu.Data("❌ 取消", cbCancel)
menu.Inline(menu.Row(confirmBtn, cancelBtn))
text := fmt.Sprintf("📋 确认添加?\n\n频道: %s\n分类: %s\n标题: %s", channelName, state.SelectedCat, title)
return c.Reply(text, menu)
}
func (b *Bot) handleCategoryCallback(c tele.Context, userID int64, category string) error {
state := b.states.Get(userID)
if state == nil || state.Step != StepAwaitCategory {
@@ -124,17 +291,20 @@ func (b *Bot) handleCategoryCallback(c tele.Context, userID int64, category stri
b.states.SetCategory(userID, category)
channelName := "未知频道"
if state.ForwardedMsg.OriginalChat != nil {
channelName = state.ForwardedMsg.OriginalChat.Title
}
// 提取默认标题
defaultTitle := extractTitle(state.ForwardedMsg)
menu := &tele.ReplyMarkup{}
confirmBtn := menu.Data("✅ 确认", cbPrefixConfirm+"yes")
useDefaultBtn := menu.Data("✅ 使用此标题", cbPrefixTitle+"default")
customBtn := menu.Data("✏️ 自定义标题", cbPrefixTitle+"custom")
cancelBtn := menu.Data("❌ 取消", cbCancel)
menu.Inline(menu.Row(confirmBtn, cancelBtn))
menu.Inline(
menu.Row(useDefaultBtn),
menu.Row(customBtn),
menu.Row(cancelBtn),
)
text := fmt.Sprintf("📋 确认添加?\n\n频道: %s\n分类: %s", channelName, category)
text := fmt.Sprintf("📝 确认标题\n\n分类: %s\n标题: %s\n\n使用此标题或自定义?", category, defaultTitle)
c.Edit(text, menu)
return c.Respond()
}
@@ -152,8 +322,18 @@ func (b *Bot) handleConfirmCallback(c tele.Context, userID int64, action string)
defer b.states.Delete(userID)
msg := state.ForwardedMsg
title := extractTitle(msg)
link := buildMessageLink(msg)
title := state.Title
// 复制消息内容发送到频道(非转发,可编辑)
channel := &tele.Chat{ID: b.cfg.Channel.ID}
channelMsg, err := b.sendMessageCopy(channel, msg)
if err != nil {
c.Edit(fmt.Sprintf("❌ 发送到频道失败: %v", err))
return c.Respond(&tele.CallbackResponse{Text: "发送失败"})
}
// 使用频道消息的链接
link := buildChannelLink(b.cfg.Channel.ID, channelMsg.ID)
entry, err := b.storage.CreateEntry(state.SelectedCat, title, link)
if err != nil {
@@ -168,6 +348,66 @@ func (b *Bot) handleConfirmCallback(c tele.Context, userID int64, action string)
return c.Respond(&tele.CallbackResponse{Text: "添加成功"})
}
// sendMessageCopy 复制消息内容发送(非转发,可编辑)
func (b *Bot) sendMessageCopy(to *tele.Chat, msg *tele.Message) (*tele.Message, error) {
// 图片
if msg.Photo != nil {
photo := &tele.Photo{File: msg.Photo.File, Caption: msg.Caption}
return b.bot.Send(to, photo, tele.ModeHTML)
}
// 视频
if msg.Video != nil {
video := &tele.Video{File: msg.Video.File, Caption: msg.Caption}
return b.bot.Send(to, video, tele.ModeHTML)
}
// 文档
if msg.Document != nil {
doc := &tele.Document{File: msg.Document.File, Caption: msg.Caption}
return b.bot.Send(to, doc, tele.ModeHTML)
}
// 音频
if msg.Audio != nil {
audio := &tele.Audio{File: msg.Audio.File, Caption: msg.Caption}
return b.bot.Send(to, audio, tele.ModeHTML)
}
// 语音
if msg.Voice != nil {
voice := &tele.Voice{File: msg.Voice.File}
return b.bot.Send(to, voice)
}
// 贴纸
if msg.Sticker != nil {
sticker := &tele.Sticker{File: msg.Sticker.File}
return b.bot.Send(to, sticker)
}
// 动图
if msg.Animation != nil {
anim := &tele.Animation{File: msg.Animation.File, Caption: msg.Caption}
return b.bot.Send(to, anim, tele.ModeHTML)
}
// 纯文本
if msg.Text != "" {
return b.bot.Send(to, msg.Text, tele.ModeHTML, tele.NoPreview)
}
return nil, fmt.Errorf("不支持的消息类型")
}
func buildChannelLink(channelID int64, msgID int) string {
chatID := channelID
if chatID < 0 {
chatID = -chatID - 1000000000000
}
return fmt.Sprintf("https://t.me/c/%d/%d", chatID, msgID)
}
func extractTitle(msg *tele.Message) string {
text := msg.Text
if text == "" {

View File

@@ -4,13 +4,40 @@ import (
tele "gopkg.in/telebot.v3"
)
// isAdmin 检查用户是否为管理员(配置文件或数据库)
func (b *Bot) isAdmin(userID int64) bool {
return b.cfg.IsAdmin(userID) || b.storage.IsAdmin(userID)
}
func (b *Bot) AdminMiddleware() tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
if !b.cfg.IsAdmin(c.Sender().ID) {
// 群组中的非命令消息,非管理员直接忽略不回复
if c.Chat().Type != tele.ChatPrivate {
msg := c.Message()
if msg != nil && !msg.IsService() && (msg.Text == "" || msg.Text[0] != '/') {
if !b.isAdmin(c.Sender().ID) {
return nil // 静默忽略
}
}
}
if !b.isAdmin(c.Sender().ID) {
return c.Reply("⛔ 无权限访问")
}
return next(c)
}
}
}
// SuperAdminMiddleware 超级管理员中间件(仅配置文件中的管理员)
func (b *Bot) SuperAdminMiddleware() tele.MiddlewareFunc {
return func(next tele.HandlerFunc) tele.HandlerFunc {
return func(c tele.Context) error {
if !b.cfg.IsSuperAdmin(c.Sender().ID) {
return c.Reply("⛔ 需要超级管理员权限")
}
return next(c)
}
}
}

View File

@@ -12,6 +12,7 @@ type PostStep int
const (
StepAwaitForward PostStep = iota
StepAwaitCategory
StepAwaitTitle
StepAwaitConfirm
)
@@ -19,6 +20,7 @@ type PostState struct {
UserID int64
ForwardedMsg *tele.Message
SelectedCat string
Title string
Step PostStep
CreatedAt time.Time
}
@@ -86,6 +88,20 @@ func (sm *StateManager) SetCategory(userID int64, category string) *PostState {
}
state.SelectedCat = category
state.Step = StepAwaitTitle
return state
}
func (sm *StateManager) SetTitle(userID int64, title string) *PostState {
sm.mu.Lock()
defer sm.mu.Unlock()
state := sm.states[userID]
if state == nil {
return nil
}
state.Title = title
state.Step = StepAwaitConfirm
return state
}

View File

@@ -2,6 +2,7 @@ package toc
import (
"log"
"strings"
"sync"
"time"
@@ -86,10 +87,12 @@ func (m *Manager) updateMessage(content string) error {
_, err = m.bot.Edit(existingMsg, content, tele.ModeMarkdown, tele.NoPreview)
if err != nil {
if err == tele.ErrMessageNotModified {
errMsg := err.Error()
// 内容未变化,忽略
if err == tele.ErrMessageNotModified || strings.Contains(errMsg, "message is not modified") {
return nil
}
if err.Error() == "telegram: message to edit not found (400)" {
if strings.Contains(errMsg, "message to edit not found") {
msg, err := m.bot.Send(chat, content, tele.ModeMarkdown, tele.NoPreview)
if err != nil {
return err