* refactor: a big refactor. wip * refactor: port handle file * refactor: place all handlers * fix: task info nil pointer * feat: enhance task progress tracking and context management * feat: cancel task * feat: stream mode * feat: silent mode * feat: dir cmd * refactor: remove unused old file * feat: rule cmd * feat: handle silent mode * feat: batch task * fix: batch task progress and temp file cleanup * refactor: update file creation and cleanup methods for better resource management * feat: add save command with silent mode handling * feat: message link * feat: update message prompts to include file count in storage selection * feat: slient save links * refactor: reduce dup code * feat: rule type * feat: chose dir * feat: refactor file handling and storage rules, improve error handling and logging * feat: rule mode * feat: telegraph pics * fix: tphpics nil pointer and inaccurate dirpath * feat: silent save telegraph * feat: add suffix to avoid file overwrite * feat: new storage telegram * chore: tidy go mod
184 lines
4.5 KiB
Go
184 lines
4.5 KiB
Go
package tgutil
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/celestix/gotgproto/ext"
|
|
"github.com/duke-git/lancet/v2/maputil"
|
|
"github.com/duke-git/lancet/v2/mathutil"
|
|
"github.com/duke-git/lancet/v2/slice"
|
|
lcstrutil "github.com/duke-git/lancet/v2/strutil"
|
|
"github.com/duke-git/lancet/v2/validator"
|
|
"github.com/gabriel-vasile/mimetype"
|
|
"github.com/gotd/td/tg"
|
|
"github.com/krau/SaveAny-Bot/common/cache"
|
|
"github.com/krau/SaveAny-Bot/common/utils/strutil"
|
|
"github.com/rs/xid"
|
|
)
|
|
|
|
func GenFileNameFromMessage(message tg.Message) string {
|
|
ext := func(media tg.MessageMediaClass) string {
|
|
switch media := media.(type) {
|
|
case *tg.MessageMediaDocument:
|
|
doc, ok := media.Document.AsNotEmpty()
|
|
if !ok {
|
|
return ""
|
|
}
|
|
ext := mimetype.Lookup(doc.MimeType).Extension()
|
|
if ext == "" {
|
|
return ""
|
|
}
|
|
return ext
|
|
case *tg.MessageMediaPhoto:
|
|
return ".jpg"
|
|
}
|
|
return ""
|
|
}(message.Media)
|
|
text := strings.TrimSpace(message.GetMessage())
|
|
if text == "" {
|
|
return fmt.Sprintf("%d_%s%s", message.GetID(), xid.New().String(), ext)
|
|
}
|
|
filename := func() string {
|
|
tags := strutil.ExtractTagsFromText(text)
|
|
if len(tags) > 0 {
|
|
tagStrRunes := make([]rune, 0, 64)
|
|
for i, tag := range tags {
|
|
if i > 0 {
|
|
tagStrRunes = append(tagStrRunes, '_')
|
|
}
|
|
tagStrRunes = append(tagStrRunes, []rune(tag)...)
|
|
if len(tagStrRunes) >= 64 {
|
|
break
|
|
}
|
|
}
|
|
tagStr := string(tagStrRunes)
|
|
return fmt.Sprintf("%s_%s", tagStr, strconv.Itoa(message.GetID()))
|
|
}
|
|
text = lcstrutil.Substring(strings.Map(func(r rune) rune {
|
|
if r < 0x20 || r == 0x7F {
|
|
return '_'
|
|
}
|
|
switch r {
|
|
// invalid characters
|
|
case '/', '\\',
|
|
':', '*', '?', '"', '<', '>', '|':
|
|
return '_'
|
|
// empty
|
|
case ' ', '\t', '\r', '\n':
|
|
return '_'
|
|
}
|
|
if validator.IsPrintable(string(r)) {
|
|
return r
|
|
}
|
|
return '_'
|
|
}, text), 0, 64)
|
|
text = strings.Join(strings.FieldsFunc(text, func(r rune) bool {
|
|
return r == '_' || r == ' '
|
|
}), "_")
|
|
return text
|
|
}()
|
|
|
|
if filename == "" {
|
|
filename = fmt.Sprintf("%d_%s", message.GetID(), xid.New().String())
|
|
}
|
|
return filename + ext
|
|
}
|
|
|
|
func BuildCancelButton(taskID string) tg.KeyboardButtonClass {
|
|
return &tg.KeyboardButtonCallback{
|
|
Text: "取消任务",
|
|
Data: fmt.Appendf(nil, "cancel %s", taskID),
|
|
}
|
|
}
|
|
|
|
func InputMessageClassSliceFromInt(ids []int) []tg.InputMessageClass {
|
|
result := make([]tg.InputMessageClass, 0, len(ids))
|
|
for _, id := range ids {
|
|
result = append(result, &tg.InputMessageID{
|
|
ID: id,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func GetMessagesRange(ctx *ext.Context, chatID int64, minId, maxId int) ([]*tg.Message, error) {
|
|
if minId > maxId {
|
|
return nil, fmt.Errorf("minId (%d) cannot be greater than maxId (%d)", minId, maxId)
|
|
}
|
|
total := maxId - minId + 1
|
|
msgIds := mathutil.Range(minId, total)
|
|
toFetchIds := make([]int, 0, total)
|
|
cached := make(map[int]*tg.Message, total)
|
|
for _, id := range msgIds {
|
|
if msg, ok := cache.Get[*tg.Message](fmt.Sprintf("tgmsg:%d:%d:%d", ctx.Self.ID, chatID, id)); ok {
|
|
cached[id] = msg
|
|
} else {
|
|
toFetchIds = append(toFetchIds, id)
|
|
}
|
|
}
|
|
if len(toFetchIds) == 0 {
|
|
return maputil.Values(cached), nil
|
|
}
|
|
|
|
result := make([]*tg.Message, 0, total)
|
|
chunks := slice.Chunk(toFetchIds, 100)
|
|
for _, chunk := range chunks {
|
|
msgs, err := ctx.GetMessages(chatID, InputMessageClassSliceFromInt(chunk))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(msgs) == 0 {
|
|
continue
|
|
}
|
|
for _, msg := range msgs {
|
|
if msg == nil {
|
|
continue
|
|
}
|
|
tgMessage, ok := msg.(*tg.Message)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if tgMessage.GetID() < minId || tgMessage.GetID() > maxId {
|
|
continue
|
|
}
|
|
result = append(result, tgMessage)
|
|
}
|
|
}
|
|
|
|
for _, msg := range result {
|
|
cache.Set(fmt.Sprintf("tgmsg:%d:%d:%d", ctx.Self.ID, chatID, msg.GetID()), msg)
|
|
}
|
|
for _, msg := range cached {
|
|
if msg == nil {
|
|
continue
|
|
}
|
|
result = append(result, msg)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func GetMessageByID(ctx *ext.Context, chatID int64, msgID int) (*tg.Message, error) {
|
|
key := fmt.Sprintf("tgmsg:%d:%d:%d", ctx.Self.ID, chatID, msgID)
|
|
if msg, ok := cache.Get[*tg.Message](key); ok {
|
|
return msg, nil
|
|
}
|
|
msgs, err := ctx.GetMessages(chatID, []tg.InputMessageClass{
|
|
&tg.InputMessageID{ID: msgID},
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get message by ID: %w", err)
|
|
}
|
|
if len(msgs) == 0 {
|
|
return nil, fmt.Errorf("message not found: chatID=%d, msgID=%d", chatID, msgID)
|
|
}
|
|
msg := msgs[0]
|
|
tgm, ok := msg.(*tg.Message)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected message type: %T", msg)
|
|
}
|
|
cache.Set(key, tgm)
|
|
return tgm, nil
|
|
}
|