main #2
@@ -1,14 +1,14 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Storage) CreateEntry(category, title, channelLink, directLink string) (*Entry, error) {
|
func (s *Storage) CreateEntry(category, title, channelLink, directLink string) (*Entry, error) {
|
||||||
entry := &Entry{
|
entry := &Entry{
|
||||||
ID: shortid.MustGenerate(),
|
ID: shortid.MustGenerate(),
|
||||||
@@ -19,131 +19,131 @@ func (s *Storage) CreateEntry(category, title, channelLink, directLink string) (
|
|||||||
DirectLink: directLink,
|
DirectLink: directLink,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.db.Update(func(tx *bolt.Tx) error {
|
err := s.db.Update(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketEntries)
|
b := tx.Bucket(bucketEntries)
|
||||||
data, err := encodeJSON(entry)
|
data, err := encodeJSON(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return b.Put([]byte(entry.ID), data)
|
return b.Put([]byte(entry.ID), data)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) GetEntry(id string) (*Entry, error) {
|
func (s *Storage) GetEntry(id string) (*Entry, error) {
|
||||||
var entry *Entry
|
var entry *Entry
|
||||||
err := s.db.View(func(tx *bolt.Tx) error {
|
err := s.db.View(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketEntries)
|
b := tx.Bucket(bucketEntries)
|
||||||
data := b.Get([]byte(id))
|
data := b.Get([]byte(id))
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return fmt.Errorf("entry %q not found", id)
|
return fmt.Errorf("entry %q not found", id)
|
||||||
}
|
}
|
||||||
entry = &Entry{}
|
entry = &Entry{}
|
||||||
return decodeJSON(data, entry)
|
return decodeJSON(data, entry)
|
||||||
})
|
})
|
||||||
return entry, err
|
return entry, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) DeleteEntry(id string) error {
|
func (s *Storage) DeleteEntry(id string) error {
|
||||||
return s.db.Update(func(tx *bolt.Tx) error {
|
return s.db.Update(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketEntries)
|
b := tx.Bucket(bucketEntries)
|
||||||
if b.Get([]byte(id)) == nil {
|
if b.Get([]byte(id)) == nil {
|
||||||
return fmt.Errorf("entry %q not found", id)
|
return fmt.Errorf("entry %q not found", id)
|
||||||
}
|
}
|
||||||
return b.Delete([]byte(id))
|
return b.Delete([]byte(id))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) UpdateEntryTitle(id, title string) error {
|
func (s *Storage) UpdateEntryTitle(id, title string) error {
|
||||||
return s.db.Update(func(tx *bolt.Tx) error {
|
return s.db.Update(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketEntries)
|
b := tx.Bucket(bucketEntries)
|
||||||
data := b.Get([]byte(id))
|
data := b.Get([]byte(id))
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return fmt.Errorf("entry %q not found", id)
|
return fmt.Errorf("entry %q not found", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry Entry
|
var entry Entry
|
||||||
if err := decodeJSON(data, &entry); err != nil {
|
if err := decodeJSON(data, &entry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.Title = title
|
entry.Title = title
|
||||||
newData, err := encodeJSON(entry)
|
newData, err := encodeJSON(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.Put([]byte(id), newData)
|
return b.Put([]byte(id), newData)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) UpdateEntryCategory(id, category string) error {
|
func (s *Storage) UpdateEntryCategory(id, category string) error {
|
||||||
return s.db.Update(func(tx *bolt.Tx) error {
|
return s.db.Update(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketEntries)
|
b := tx.Bucket(bucketEntries)
|
||||||
data := b.Get([]byte(id))
|
data := b.Get([]byte(id))
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return fmt.Errorf("entry %q not found", id)
|
return fmt.Errorf("entry %q not found", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
var entry Entry
|
var entry Entry
|
||||||
if err := decodeJSON(data, &entry); err != nil {
|
if err := decodeJSON(data, &entry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.Category = category
|
entry.Category = category
|
||||||
newData, err := encodeJSON(entry)
|
newData, err := encodeJSON(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.Put([]byte(id), newData)
|
return b.Put([]byte(id), newData)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) ListEntries(category string) ([]Entry, error) {
|
func (s *Storage) ListEntries(category string) ([]Entry, error) {
|
||||||
var entries []Entry
|
var entries []Entry
|
||||||
err := s.db.View(func(tx *bolt.Tx) error {
|
err := s.db.View(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketEntries)
|
b := tx.Bucket(bucketEntries)
|
||||||
return b.ForEach(func(k, v []byte) error {
|
return b.ForEach(func(k, v []byte) error {
|
||||||
var entry Entry
|
var entry Entry
|
||||||
if err := decodeJSON(v, &entry); err != nil {
|
if err := decodeJSON(v, &entry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if category == "" || entry.Category == category {
|
if category == "" || entry.Category == category {
|
||||||
entries = append(entries, entry)
|
entries = append(entries, entry)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(entries, func(i, j int) bool {
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
return entries[i].Timestamp.Before(entries[j].Timestamp)
|
return entries[i].Timestamp.Before(entries[j].Timestamp)
|
||||||
})
|
})
|
||||||
|
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) ListAllEntries() ([]Entry, error) {
|
func (s *Storage) ListAllEntries() ([]Entry, error) {
|
||||||
return s.ListEntries("")
|
return s.ListEntries("")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) GetEntriesByCategory() (map[string][]Entry, error) {
|
func (s *Storage) GetEntriesByCategory() (map[string][]Entry, error) {
|
||||||
entries, err := s.ListAllEntries()
|
entries, err := s.ListAllEntries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string][]Entry)
|
result := make(map[string][]Entry)
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
result[e.Category] = append(result[e.Category], e)
|
result[e.Category] = append(result[e.Category], e)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -8,25 +8,25 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
bucketAppConfig = []byte("AppConfig")
|
bucketAppConfig = []byte("AppConfig")
|
||||||
bucketCategories = []byte("Categories")
|
bucketCategories = []byte("Categories")
|
||||||
bucketEntries = []byte("Entries")
|
bucketEntries = []byte("Entries")
|
||||||
bucketAdmins = []byte("Admins")
|
bucketAdmins = []byte("Admins")
|
||||||
|
|
||||||
keyTocMsgID = []byte("toc_msg_id")
|
keyTocMsgID = []byte("toc_msg_id")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Category struct {
|
type Category struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Order int `json:"order"`
|
Order int `json:"order"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Entry struct {
|
type Entry struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
@@ -48,120 +48,120 @@ func (e Entry) HasDirectLink() bool {
|
|||||||
direct := strings.TrimSpace(e.DirectLink)
|
direct := strings.TrimSpace(e.DirectLink)
|
||||||
return direct != "" && direct != e.MessageLink()
|
return direct != "" && direct != e.MessageLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
db *bolt.DB
|
db *bolt.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(path string) (*Storage, error) {
|
func New(path string) (*Storage, error) {
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create data directory: %w", err)
|
return nil, fmt.Errorf("failed to create data directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Storage{db: db}
|
s := &Storage{db: db}
|
||||||
if err := s.initBuckets(); err != nil {
|
if err := s.initBuckets(); err != nil {
|
||||||
db.Close()
|
db.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) initBuckets() error {
|
func (s *Storage) initBuckets() error {
|
||||||
return s.db.Update(func(tx *bolt.Tx) error {
|
return s.db.Update(func(tx *bolt.Tx) error {
|
||||||
buckets := [][]byte{bucketAppConfig, bucketCategories, bucketEntries, bucketAdmins}
|
buckets := [][]byte{bucketAppConfig, bucketCategories, bucketEntries, bucketAdmins}
|
||||||
for _, b := range buckets {
|
for _, b := range buckets {
|
||||||
if _, err := tx.CreateBucketIfNotExists(b); err != nil {
|
if _, err := tx.CreateBucketIfNotExists(b); err != nil {
|
||||||
return fmt.Errorf("failed to create bucket %s: %w", b, err)
|
return fmt.Errorf("failed to create bucket %s: %w", b, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) Close() error {
|
func (s *Storage) Close() error {
|
||||||
return s.db.Close()
|
return s.db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) GetTocMsgID() (int, error) {
|
func (s *Storage) GetTocMsgID() (int, error) {
|
||||||
var msgID int
|
var msgID int
|
||||||
err := s.db.View(func(tx *bolt.Tx) error {
|
err := s.db.View(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketAppConfig)
|
b := tx.Bucket(bucketAppConfig)
|
||||||
v := b.Get(keyTocMsgID)
|
v := b.Get(keyTocMsgID)
|
||||||
if v != nil && len(v) >= 4 {
|
if v != nil && len(v) >= 4 {
|
||||||
msgID = int(binary.BigEndian.Uint32(v))
|
msgID = int(binary.BigEndian.Uint32(v))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return msgID, err
|
return msgID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) SetTocMsgID(msgID int) error {
|
func (s *Storage) SetTocMsgID(msgID int) error {
|
||||||
return s.db.Update(func(tx *bolt.Tx) error {
|
return s.db.Update(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketAppConfig)
|
b := tx.Bucket(bucketAppConfig)
|
||||||
buf := make([]byte, 4)
|
buf := make([]byte, 4)
|
||||||
binary.BigEndian.PutUint32(buf, uint32(msgID))
|
binary.BigEndian.PutUint32(buf, uint32(msgID))
|
||||||
return b.Put(keyTocMsgID, buf)
|
return b.Put(keyTocMsgID, buf)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeJSON(v any) ([]byte, error) {
|
func encodeJSON(v any) ([]byte, error) {
|
||||||
return json.Marshal(v)
|
return json.Marshal(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeJSON(data []byte, v any) error {
|
func decodeJSON(data []byte, v any) error {
|
||||||
return json.Unmarshal(data, v)
|
return json.Unmarshal(data, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin management
|
// Admin management
|
||||||
|
|
||||||
func (s *Storage) AddAdmin(userID int64) error {
|
func (s *Storage) AddAdmin(userID int64) error {
|
||||||
return s.db.Update(func(tx *bolt.Tx) error {
|
return s.db.Update(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketAdmins)
|
b := tx.Bucket(bucketAdmins)
|
||||||
key := make([]byte, 8)
|
key := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(key, uint64(userID))
|
binary.BigEndian.PutUint64(key, uint64(userID))
|
||||||
return b.Put(key, []byte("1"))
|
return b.Put(key, []byte("1"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) RemoveAdmin(userID int64) error {
|
func (s *Storage) RemoveAdmin(userID int64) error {
|
||||||
return s.db.Update(func(tx *bolt.Tx) error {
|
return s.db.Update(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketAdmins)
|
b := tx.Bucket(bucketAdmins)
|
||||||
key := make([]byte, 8)
|
key := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(key, uint64(userID))
|
binary.BigEndian.PutUint64(key, uint64(userID))
|
||||||
return b.Delete(key)
|
return b.Delete(key)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) IsAdmin(userID int64) bool {
|
func (s *Storage) IsAdmin(userID int64) bool {
|
||||||
var exists bool
|
var exists bool
|
||||||
s.db.View(func(tx *bolt.Tx) error {
|
s.db.View(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketAdmins)
|
b := tx.Bucket(bucketAdmins)
|
||||||
key := make([]byte, 8)
|
key := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(key, uint64(userID))
|
binary.BigEndian.PutUint64(key, uint64(userID))
|
||||||
exists = b.Get(key) != nil
|
exists = b.Get(key) != nil
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return exists
|
return exists
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) ListAdmins() ([]int64, error) {
|
func (s *Storage) ListAdmins() ([]int64, error) {
|
||||||
var admins []int64
|
var admins []int64
|
||||||
err := s.db.View(func(tx *bolt.Tx) error {
|
err := s.db.View(func(tx *bolt.Tx) error {
|
||||||
b := tx.Bucket(bucketAdmins)
|
b := tx.Bucket(bucketAdmins)
|
||||||
return b.ForEach(func(k, v []byte) error {
|
return b.ForEach(func(k, v []byte) error {
|
||||||
if len(k) == 8 {
|
if len(k) == 8 {
|
||||||
userID := int64(binary.BigEndian.Uint64(k))
|
userID := int64(binary.BigEndian.Uint64(k))
|
||||||
admins = append(admins, userID)
|
admins = append(admins, userID)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return admins, err
|
return admins, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,172 +1,172 @@
|
|||||||
package telegram
|
package telegram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tele "gopkg.in/telebot.v3"
|
tele "gopkg.in/telebot.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
cbPrefixDelEntry = "del:"
|
cbPrefixDelEntry = "del:"
|
||||||
cbPrefixDelWithMsg = "delwithmsg:"
|
cbPrefixDelWithMsg = "delwithmsg:"
|
||||||
cbPrefixDelOnlyToc = "delonlytoc:"
|
cbPrefixDelOnlyToc = "delonlytoc:"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *Bot) handleEntryDel(c tele.Context) error {
|
func (b *Bot) handleEntryDel(c tele.Context) error {
|
||||||
id := strings.TrimSpace(c.Message().Payload)
|
id := strings.TrimSpace(c.Message().Payload)
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return c.Reply("用法: /del <ID>")
|
return c.Reply("用法: /del <ID>")
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := b.storage.GetEntry(id)
|
entry, err := b.storage.GetEntry(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
menu := &tele.ReplyMarkup{}
|
menu := &tele.ReplyMarkup{}
|
||||||
delWithMsgBtn := menu.Data("🗑 删除条目+频道消息", cbPrefixDelWithMsg+id)
|
delWithMsgBtn := menu.Data("🗑 删除条目+频道消息", cbPrefixDelWithMsg+id)
|
||||||
delOnlyTocBtn := menu.Data("📝 仅删除目录条目", cbPrefixDelOnlyToc+id)
|
delOnlyTocBtn := menu.Data("📝 仅删除目录条目", cbPrefixDelOnlyToc+id)
|
||||||
cancelBtn := menu.Data("❌ 取消", cbCancel)
|
cancelBtn := menu.Data("❌ 取消", cbCancel)
|
||||||
menu.Inline(
|
menu.Inline(
|
||||||
menu.Row(delWithMsgBtn),
|
menu.Row(delWithMsgBtn),
|
||||||
menu.Row(delOnlyTocBtn),
|
menu.Row(delOnlyTocBtn),
|
||||||
menu.Row(cancelBtn),
|
menu.Row(cancelBtn),
|
||||||
)
|
)
|
||||||
|
|
||||||
text := fmt.Sprintf("🗑 确认删除?\n\n分类: %s\n标题: %s\n\n请选择删除方式:", entry.Category, entry.Title)
|
text := fmt.Sprintf("🗑 确认删除?\n\n分类: %s\n标题: %s\n\n请选择删除方式:", entry.Category, entry.Title)
|
||||||
return c.Reply(text, menu)
|
return c.Reply(text, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) handleEntryEdit(c tele.Context) error {
|
func (b *Bot) handleEntryEdit(c tele.Context) error {
|
||||||
args := strings.SplitN(c.Message().Payload, " ", 2)
|
args := strings.SplitN(c.Message().Payload, " ", 2)
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return c.Reply("用法: /edit <ID> <新标题>")
|
return c.Reply("用法: /edit <ID> <新标题>")
|
||||||
}
|
}
|
||||||
|
|
||||||
id := args[0]
|
id := args[0]
|
||||||
newTitle := strings.TrimSpace(args[1])
|
newTitle := strings.TrimSpace(args[1])
|
||||||
|
|
||||||
if err := b.storage.UpdateEntryTitle(id, newTitle); err != nil {
|
if err := b.storage.UpdateEntryTitle(id, newTitle); err != nil {
|
||||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
b.toc.TriggerUpdate()
|
b.toc.TriggerUpdate()
|
||||||
return c.Reply(fmt.Sprintf("✅ 标题已更新为: %s", newTitle))
|
return c.Reply(fmt.Sprintf("✅ 标题已更新为: %s", newTitle))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) handleEntryMove(c tele.Context) error {
|
func (b *Bot) handleEntryMove(c tele.Context) error {
|
||||||
args := strings.Fields(c.Message().Payload)
|
args := strings.Fields(c.Message().Payload)
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return c.Reply("用法: /move <ID> <新分类>")
|
return c.Reply("用法: /move <ID> <新分类>")
|
||||||
}
|
}
|
||||||
|
|
||||||
id := args[0]
|
id := args[0]
|
||||||
newCategory := args[1]
|
newCategory := args[1]
|
||||||
|
|
||||||
if !b.storage.CategoryExists(newCategory) {
|
if !b.storage.CategoryExists(newCategory) {
|
||||||
return c.Reply(fmt.Sprintf("❌ 分类 [%s] 不存在", newCategory))
|
return c.Reply(fmt.Sprintf("❌ 分类 [%s] 不存在", newCategory))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.storage.UpdateEntryCategory(id, newCategory); err != nil {
|
if err := b.storage.UpdateEntryCategory(id, newCategory); err != nil {
|
||||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
b.toc.TriggerUpdate()
|
b.toc.TriggerUpdate()
|
||||||
return c.Reply(fmt.Sprintf("✅ 已移动到分类: %s", newCategory))
|
return c.Reply(fmt.Sprintf("✅ 已移动到分类: %s", newCategory))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) handleEntryList(c tele.Context) error {
|
func (b *Bot) handleEntryList(c tele.Context) error {
|
||||||
category := strings.TrimSpace(c.Message().Payload)
|
category := strings.TrimSpace(c.Message().Payload)
|
||||||
|
|
||||||
entries, err := b.storage.ListEntries(category)
|
entries, err := b.storage.ListEntries(category)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Reply(fmt.Sprintf("❌ %v", err))
|
return c.Reply(fmt.Sprintf("❌ %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(entries) == 0 {
|
if len(entries) == 0 {
|
||||||
if category != "" {
|
if category != "" {
|
||||||
return c.Reply(fmt.Sprintf("📭 分类 [%s] 暂无条目", category))
|
return c.Reply(fmt.Sprintf("📭 分类 [%s] 暂无条目", category))
|
||||||
}
|
}
|
||||||
return c.Reply("📭 暂无条目")
|
return c.Reply("📭 暂无条目")
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
if category != "" {
|
if category != "" {
|
||||||
sb.WriteString(fmt.Sprintf("📋 分类 [%s] 的条目:\n\n", html.EscapeString(category)))
|
sb.WriteString(fmt.Sprintf("📋 分类 [%s] 的条目:\n\n", html.EscapeString(category)))
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString("📋 所有条目:\n\n")
|
sb.WriteString("📋 所有条目:\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCat := ""
|
currentCat := ""
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if category == "" && e.Category != currentCat {
|
if category == "" && e.Category != currentCat {
|
||||||
currentCat = e.Category
|
currentCat = e.Category
|
||||||
sb.WriteString(fmt.Sprintf("\n【%s】\n", html.EscapeString(currentCat)))
|
sb.WriteString(fmt.Sprintf("\n【%s】\n", html.EscapeString(currentCat)))
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("• <code>%s</code> %s\n", e.ID, html.EscapeString(e.Title)))
|
sb.WriteString(fmt.Sprintf("• <code>%s</code> %s\n", e.ID, html.EscapeString(e.Title)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Reply(sb.String(), tele.ModeHTML)
|
return c.Reply(sb.String(), tele.ModeHTML)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) handleRefresh(c tele.Context) error {
|
func (b *Bot) handleRefresh(c tele.Context) error {
|
||||||
b.toc.TriggerUpdate()
|
b.toc.TriggerUpdate()
|
||||||
return c.Reply("🔄 目录刷新已触发")
|
return c.Reply("🔄 目录刷新已触发")
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleDeleteEntryCallback 处理删除条目的回调
|
// handleDeleteEntryCallback 处理删除条目的回调
|
||||||
func (b *Bot) handleDeleteEntryCallback(c tele.Context, entryID string, deleteChannelMsg bool) error {
|
func (b *Bot) handleDeleteEntryCallback(c tele.Context, entryID string, deleteChannelMsg bool) error {
|
||||||
entry, err := b.storage.GetEntry(entryID)
|
entry, err := b.storage.GetEntry(entryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Edit(fmt.Sprintf("❌ %v", err))
|
c.Edit(fmt.Sprintf("❌ %v", err))
|
||||||
return c.Respond(&tele.CallbackResponse{Text: "条目不存在"})
|
return c.Respond(&tele.CallbackResponse{Text: "条目不存在"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据用户选择决定是否删除频道消息
|
// 根据用户选择决定是否删除频道消息
|
||||||
msgDeleted := false
|
msgDeleted := false
|
||||||
if deleteChannelMsg {
|
if deleteChannelMsg {
|
||||||
if msgID := parseMessageIDFromLink(entry.MessageLink()); msgID != 0 {
|
if msgID := parseMessageIDFromLink(entry.MessageLink()); msgID != 0 {
|
||||||
channel := &tele.Chat{ID: b.cfg.Channel.ID}
|
channel := &tele.Chat{ID: b.cfg.Channel.ID}
|
||||||
msg := &tele.Message{ID: msgID, Chat: channel}
|
msg := &tele.Message{ID: msgID, Chat: channel}
|
||||||
if err := b.bot.Delete(msg); err == nil {
|
if err := b.bot.Delete(msg); err == nil {
|
||||||
msgDeleted = true
|
msgDeleted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.storage.DeleteEntry(entryID); err != nil {
|
if err := b.storage.DeleteEntry(entryID); err != nil {
|
||||||
c.Edit(fmt.Sprintf("❌ 删除失败: %v", err))
|
c.Edit(fmt.Sprintf("❌ 删除失败: %v", err))
|
||||||
return c.Respond(&tele.CallbackResponse{Text: "删除失败"})
|
return c.Respond(&tele.CallbackResponse{Text: "删除失败"})
|
||||||
}
|
}
|
||||||
|
|
||||||
b.toc.TriggerUpdate()
|
b.toc.TriggerUpdate()
|
||||||
|
|
||||||
var text string
|
var text string
|
||||||
if deleteChannelMsg && msgDeleted {
|
if deleteChannelMsg && msgDeleted {
|
||||||
text = fmt.Sprintf("✅ 已删除: [%s] %s\n📨 频道消息已同步删除", entry.Category, entry.Title)
|
text = fmt.Sprintf("✅ 已删除: [%s] %s\n📨 频道消息已同步删除", entry.Category, entry.Title)
|
||||||
} else if deleteChannelMsg && !msgDeleted {
|
} else if deleteChannelMsg && !msgDeleted {
|
||||||
text = fmt.Sprintf("✅ 已删除: [%s] %s\n⚠️ 频道消息删除失败(可能已被删除)", entry.Category, entry.Title)
|
text = fmt.Sprintf("✅ 已删除: [%s] %s\n⚠️ 频道消息删除失败(可能已被删除)", entry.Category, entry.Title)
|
||||||
} else {
|
} else {
|
||||||
text = fmt.Sprintf("✅ 已删除: [%s] %s\n📝 频道消息已保留", entry.Category, entry.Title)
|
text = fmt.Sprintf("✅ 已删除: [%s] %s\n📝 频道消息已保留", entry.Category, entry.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Edit(text)
|
c.Edit(text)
|
||||||
return c.Respond(&tele.CallbackResponse{Text: "删除成功"})
|
return c.Respond(&tele.CallbackResponse{Text: "删除成功"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseMessageIDFromLink 从链接中解析消息ID
|
// parseMessageIDFromLink 从链接中解析消息ID
|
||||||
// 支持格式: https://t.me/c/123456/789 或 https://t.me/username/789
|
// 支持格式: https://t.me/c/123456/789 或 https://t.me/username/789
|
||||||
func parseMessageIDFromLink(link string) int {
|
func parseMessageIDFromLink(link string) int {
|
||||||
parts := strings.Split(link, "/")
|
parts := strings.Split(link, "/")
|
||||||
if len(parts) < 1 {
|
if len(parts) < 1 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
msgID, err := strconv.Atoi(parts[len(parts)-1])
|
msgID, err := strconv.Atoi(parts[len(parts)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return msgID
|
return msgID
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,40 +1,40 @@
|
|||||||
package toc
|
package toc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Manager) Render() (string, error) {
|
func (m *Manager) Render() (string, error) {
|
||||||
categories, err := m.storage.ListCategories()
|
categories, err := m.storage.ListCategories()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
entriesByCategory, err := m.storage.GetEntriesByCategory()
|
entriesByCategory, err := m.storage.GetEntriesByCategory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("📚 <b>频道目录</b>\n")
|
sb.WriteString("📚 <b>频道目录</b>\n")
|
||||||
sb.WriteString("━━━━━━━━━━━━━━━\n\n")
|
sb.WriteString("━━━━━━━━━━━━━━━\n\n")
|
||||||
|
|
||||||
if len(categories) == 0 {
|
if len(categories) == 0 {
|
||||||
sb.WriteString("<i>暂无分类</i>")
|
sb.WriteString("<i>暂无分类</i>")
|
||||||
return sb.String(), nil
|
return sb.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cat := range categories {
|
for _, cat := range categories {
|
||||||
entries := entriesByCategory[cat.Name]
|
entries := entriesByCategory[cat.Name]
|
||||||
|
|
||||||
sb.WriteString(fmt.Sprintf("📁 <b>%s</b>", html.EscapeString(cat.Name)))
|
sb.WriteString(fmt.Sprintf("📁 <b>%s</b>", html.EscapeString(cat.Name)))
|
||||||
if len(entries) > 0 {
|
if len(entries) > 0 {
|
||||||
sb.WriteString(fmt.Sprintf(" (%d)", len(entries)))
|
sb.WriteString(fmt.Sprintf(" (%d)", len(entries)))
|
||||||
}
|
}
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|
||||||
if len(entries) == 0 {
|
if len(entries) == 0 {
|
||||||
sb.WriteString(" <i>暂无内容</i>\n")
|
sb.WriteString(" <i>暂无内容</i>\n")
|
||||||
} else {
|
} else {
|
||||||
@@ -46,10 +46,10 @@ func (m *Manager) Render() (string, error) {
|
|||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString("━━━━━━━━━━━━━━━\n")
|
sb.WriteString("━━━━━━━━━━━━━━━\n")
|
||||||
|
|
||||||
return sb.String(), nil
|
return sb.String(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user