mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-06-09 09:29:57 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c58fa454bb | ||
|
|
2c5d6f0e57 | ||
|
|
7d57ad30a9 | ||
|
|
4f314bd37f | ||
|
|
131dfeb4cd | ||
|
|
3f40acff55 | ||
|
|
fe47ee3b51 |
@@ -43,7 +43,7 @@ func handleAddCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
|
|
||||||
if !data.SettedDir && len(dirs) != 0 {
|
if !data.SettedDir && len(dirs) != 0 {
|
||||||
// ask for directory selection
|
// ask for directory selection
|
||||||
markup, err := msgelem.BuildSetDirKeyboard(dirs, dataid)
|
markup, err := msgelem.BuildSetDirMarkupForAdd(dirs, dataid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build directory keyboard: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build directory keyboard: %s", err)
|
||||||
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, "目录键盘构建失败: "+err.Error()))
|
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, "目录键盘构建失败: "+err.Error()))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
@@ -56,7 +57,7 @@ func handleSilentSaveLink(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
userId := update.GetUserChat().GetID()
|
userId := update.GetUserChat().GetID()
|
||||||
if len(files) == 1 {
|
if len(files) == 1 {
|
||||||
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userId, stor, "", files[0], replied.ID)
|
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userId, stor, dirutil.PathFromContext(ctx), files[0], replied.ID)
|
||||||
}
|
}
|
||||||
return shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userId, stor, "", files, replied.ID)
|
return shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userId, stor, dirutil.PathFromContext(ctx), files, replied.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,12 +25,6 @@ func handleMediaMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// tfOpts := make([]tfile.TGFileOption, 0)
|
|
||||||
// switch userDB.FilenameStrategy {
|
|
||||||
// case fnamest.Message.String():
|
|
||||||
// tfOpts = append(tfOpts, tfile.WithName(tgutil.GenFileNameFromMessage(*message)))
|
|
||||||
// default:
|
|
||||||
// }
|
|
||||||
tfOpts := mediautil.TfileOptions(ctx, userDB, message)
|
tfOpts := mediautil.TfileOptions(ctx, userDB, message)
|
||||||
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, message, tfOpts...)
|
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, message, tfOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,109 +61,10 @@ func handleSilentSaveMedia(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// tfOpts := make([]tfile.TGFileOption, 0)
|
|
||||||
// switch userDB.FilenameStrategy {
|
|
||||||
// case fnamest.Message.String():
|
|
||||||
// tfOpts = append(tfOpts, tfile.WithName(tgutil.GenFileNameFromMessage(*message)))
|
|
||||||
// default:
|
|
||||||
// }
|
|
||||||
tfOpts := mediautil.TfileOptions(ctx, userDB, message)
|
tfOpts := mediautil.TfileOptions(ctx, userDB, message)
|
||||||
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, message, tfOpts...)
|
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, message, tfOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userID, stor, "", file, msg.ID)
|
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userID, stor, dirutil.PathFromContext(ctx), file, msg.ID)
|
||||||
}
|
|
||||||
|
|
||||||
type MediaGroupHandler struct {
|
|
||||||
groups map[int64][]tfile.TGFileMessage
|
|
||||||
timers map[int64]*time.Timer
|
|
||||||
mu sync.Mutex
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
var mediaGroupHandler = &MediaGroupHandler{
|
|
||||||
groups: make(map[int64][]tfile.TGFileMessage),
|
|
||||||
timers: make(map[int64]*time.Timer),
|
|
||||||
timeout: 1 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleGroupMediaMessage(ctx *ext.Context, update *ext.Update, message *tg.Message, groupID int64) error {
|
|
||||||
logger := log.FromContext(ctx)
|
|
||||||
media := message.Media
|
|
||||||
supported := mediautil.IsSupported(media)
|
|
||||||
if !supported {
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
file, err := tfile.FromMediaMessage(media, ctx.Raw, message, tfile.WithNameIfEmpty(
|
|
||||||
tgutil.GenFileNameFromMessage(*message),
|
|
||||||
))
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to get file from media: %s", err)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
mediaGroupHandler.mu.Lock()
|
|
||||||
defer mediaGroupHandler.mu.Unlock()
|
|
||||||
if mediaGroupHandler.groups[groupID] == nil {
|
|
||||||
mediaGroupHandler.groups[groupID] = make([]tfile.TGFileMessage, 0)
|
|
||||||
}
|
|
||||||
mediaGroupHandler.groups[groupID] = append(mediaGroupHandler.groups[groupID], file)
|
|
||||||
|
|
||||||
if timer, exists := mediaGroupHandler.timers[groupID]; exists {
|
|
||||||
timer.Stop()
|
|
||||||
}
|
|
||||||
mediaGroupHandler.timers[groupID] = time.AfterFunc(mediaGroupHandler.timeout, func() {
|
|
||||||
processMediaGroup(ctx, update, groupID)
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
func processMediaGroup(ctx *ext.Context, update *ext.Update, groupID int64) {
|
|
||||||
logger := log.FromContext(ctx)
|
|
||||||
mediaGroupHandler.mu.Lock()
|
|
||||||
items := mediaGroupHandler.groups[groupID]
|
|
||||||
delete(mediaGroupHandler.groups, groupID)
|
|
||||||
delete(mediaGroupHandler.timers, groupID)
|
|
||||||
mediaGroupHandler.mu.Unlock()
|
|
||||||
if len(items) == 0 {
|
|
||||||
logger.Warn("No media items to process for group", "groupID", groupID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Debugf("Processing media group %d with %d items", groupID, len(items))
|
|
||||||
|
|
||||||
userId := update.GetUserChat().GetID()
|
|
||||||
msg, err := ctx.Reply(update, ext.ReplyTextString("正在保存文件..."), nil)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to reply: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stor := storage.FromContext(ctx)
|
|
||||||
if stor != nil {
|
|
||||||
// In silent mode
|
|
||||||
if len(items) == 1 {
|
|
||||||
shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userId, stor, "", items[0], msg.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userId, stor, "", items, msg.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stors := storage.GetUserStorages(ctx, userId)
|
|
||||||
markup, err := msgelem.BuildAddSelectStorageKeyboard(stors, tcbdata.Add{
|
|
||||||
Files: items,
|
|
||||||
AsBatch: len(items) > 1,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("构建存储选择键盘失败: %s", err)
|
|
||||||
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
|
||||||
ID: msg.ID,
|
|
||||||
Message: "构建存储选择键盘失败: " + err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
|
||||||
ID: msg.ID,
|
|
||||||
Message: fmt.Sprintf("共 %d 个文件, 请选择存储位置", len(items)),
|
|
||||||
ReplyMarkup: markup,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
122
client/bot/handlers/media_group.go
Normal file
122
client/bot/handlers/media_group.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MediaGroupHandler struct {
|
||||||
|
groups map[int64][]tfile.TGFileMessage
|
||||||
|
timers map[int64]*time.Timer
|
||||||
|
mu sync.Mutex
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mediaGroupHandler *MediaGroupHandler
|
||||||
|
onceMediaGroup sync.Once
|
||||||
|
setupMediaGroupHandler = func() {
|
||||||
|
onceMediaGroup.Do(func() {
|
||||||
|
mediaGroupHandler = &MediaGroupHandler{
|
||||||
|
groups: make(map[int64][]tfile.TGFileMessage),
|
||||||
|
timers: make(map[int64]*time.Timer),
|
||||||
|
timeout: time.Duration(min(config.C().Telegram.MediaGroupTimeout, 1)) * time.Second,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleGroupMediaMessage(ctx *ext.Context, update *ext.Update, message *tg.Message, groupID int64) error {
|
||||||
|
onceMediaGroup.Do(setupMediaGroupHandler)
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
media := message.Media
|
||||||
|
supported := mediautil.IsSupported(media)
|
||||||
|
if !supported {
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
file, err := tfile.FromMediaMessage(media, ctx.Raw, message, tfile.WithNameIfEmpty(
|
||||||
|
tgutil.GenFileNameFromMessage(*message),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to get file from media: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
mediaGroupHandler.mu.Lock()
|
||||||
|
defer mediaGroupHandler.mu.Unlock()
|
||||||
|
if mediaGroupHandler.groups[groupID] == nil {
|
||||||
|
mediaGroupHandler.groups[groupID] = make([]tfile.TGFileMessage, 0)
|
||||||
|
}
|
||||||
|
mediaGroupHandler.groups[groupID] = append(mediaGroupHandler.groups[groupID], file)
|
||||||
|
|
||||||
|
if timer, exists := mediaGroupHandler.timers[groupID]; exists {
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
mediaGroupHandler.timers[groupID] = time.AfterFunc(mediaGroupHandler.timeout, func() {
|
||||||
|
processMediaGroup(ctx, update, groupID)
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func processMediaGroup(ctx *ext.Context, update *ext.Update, groupID int64) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
mediaGroupHandler.mu.Lock()
|
||||||
|
items := mediaGroupHandler.groups[groupID]
|
||||||
|
delete(mediaGroupHandler.groups, groupID)
|
||||||
|
delete(mediaGroupHandler.timers, groupID)
|
||||||
|
mediaGroupHandler.mu.Unlock()
|
||||||
|
if len(items) == 0 {
|
||||||
|
logger.Warn("No media items to process for group", "groupID", groupID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Debugf("Processing media group %d with %d items", groupID, len(items))
|
||||||
|
|
||||||
|
userId := update.GetUserChat().GetID()
|
||||||
|
msg, err := ctx.Reply(update, ext.ReplyTextString("正在保存文件..."), nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to reply: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stor := storage.FromContext(ctx)
|
||||||
|
if stor != nil {
|
||||||
|
// In silent mode
|
||||||
|
if len(items) == 1 {
|
||||||
|
shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userId, stor, "", items[0], msg.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userId, stor, "", items, msg.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stors := storage.GetUserStorages(ctx, userId)
|
||||||
|
markup, err := msgelem.BuildAddSelectStorageKeyboard(stors, tcbdata.Add{
|
||||||
|
Files: items,
|
||||||
|
AsBatch: len(items) > 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("构建存储选择键盘失败: %s", err)
|
||||||
|
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: msg.ID,
|
||||||
|
Message: "构建存储选择键盘失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: msg.ID,
|
||||||
|
Message: fmt.Sprintf("共 %d 个文件, 请选择存储位置", len(items)),
|
||||||
|
ReplyMarkup: markup,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
@@ -43,6 +44,14 @@ func handleSilentMode(next func(*ext.Context, *ext.Update) error, handler func(*
|
|||||||
ctx.Reply(update, ext.ReplyTextString("获取默认存储失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString("获取默认存储失败: "+err.Error()), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
if user.DefaultDir != 0 {
|
||||||
|
dir, err := database.GetDirByID(ctx, user.DefaultDir)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取默认文件夹失败: "+err.Error()), nil)
|
||||||
|
return next(ctx, update)
|
||||||
|
}
|
||||||
|
ctx.Context = dirutil.WithContext(ctx.Context, dir)
|
||||||
|
}
|
||||||
ctx.Context = storage.WithContext(ctx.Context, stor)
|
ctx.Context = storage.WithContext(ctx.Context, stor)
|
||||||
return handler(ctx, update)
|
return handler(ctx, update)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||||
@@ -117,5 +119,8 @@ func handleSilentSaveText(ctx *ext.Context, u *ext.Update) error {
|
|||||||
if len(item.Resources) > 1 {
|
if len(item.Resources) > 1 {
|
||||||
dirPath = fsutil.NormalizePathname(item.Title)
|
dirPath = fsutil.NormalizePathname(item.Title)
|
||||||
}
|
}
|
||||||
|
if p := dirutil.PathFromContext(ctx); p != "" {
|
||||||
|
dirPath = path.Join(p, dirPath)
|
||||||
|
}
|
||||||
return shortcut.CreateAndAddParsedTaskWithEdit(ctx, stor, dirPath, item, msg.ID, userID)
|
return shortcut.CreateAndAddParsedTaskWithEdit(ctx, stor, dirPath, item, msg.ID, userID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
@@ -87,17 +88,6 @@ func handleSilentSaveReplied(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgSaveHelpText)), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgSaveHelpText)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
// genFilename := func() string {
|
|
||||||
// if len(args) > 1 {
|
|
||||||
// return args[1]
|
|
||||||
// }
|
|
||||||
// filename := tgutil.GenFileNameFromMessage(*replyTo.Message)
|
|
||||||
// return filename
|
|
||||||
// }()
|
|
||||||
// option := tfile.WithNameIfEmpty(genFilename)
|
|
||||||
// if len(args) > 1 {
|
|
||||||
// option = tfile.WithName(genFilename)
|
|
||||||
// }
|
|
||||||
userDB, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
userDB, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -111,7 +101,7 @@ func handleSilentSaveReplied(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, update.GetUserChat().GetID(), stor, "", file, msg.GetID())
|
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, update.GetUserChat().GetID(), stor, dirutil.PathFromContext(ctx), file, msg.GetID())
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error {
|
func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
@@ -36,51 +37,71 @@ func handleSilentCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
func handleSetDefaultCallback(ctx *ext.Context, update *ext.Update) error {
|
func handleSetDefaultCallback(ctx *ext.Context, update *ext.Update) error {
|
||||||
dataid := strings.Split(string(update.CallbackQuery.Data), " ")[1]
|
dataid := strings.Split(string(update.CallbackQuery.Data), " ")[1]
|
||||||
data, ok := cache.Get[tcbdata.SetDefaultStorage](dataid)
|
data, ok := cache.Get[tcbdata.SetDefaultStorage](dataid)
|
||||||
if !ok {
|
|
||||||
|
failedAnswer := func(message string) error {
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
QueryID: update.CallbackQuery.GetQueryID(),
|
QueryID: update.CallbackQuery.GetQueryID(),
|
||||||
Alert: true,
|
Alert: true,
|
||||||
Message: "数据已过期",
|
Message: message,
|
||||||
CacheTime: 5,
|
CacheTime: 5,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return failedAnswer("数据已过期")
|
||||||
|
}
|
||||||
userID := update.CallbackQuery.GetUserID()
|
userID := update.CallbackQuery.GetUserID()
|
||||||
|
|
||||||
storageName := data.StorageName
|
storageName := data.StorageName
|
||||||
selectedStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, storageName)
|
selectedStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, storageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
return failedAnswer("存储获取失败: " + err.Error())
|
||||||
QueryID: update.CallbackQuery.GetQueryID(),
|
|
||||||
Alert: true,
|
|
||||||
Message: "存储获取失败: " + err.Error(),
|
|
||||||
CacheTime: 5,
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
}
|
||||||
user, err := database.GetUserByChatID(ctx, userID)
|
user, err := database.GetUserByChatID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
return failedAnswer("获取用户信息失败: " + err.Error())
|
||||||
QueryID: update.CallbackQuery.GetQueryID(),
|
}
|
||||||
Alert: true,
|
var dir *database.Dir
|
||||||
Message: "获取用户信息失败: " + err.Error(),
|
if data.DirID != 0 {
|
||||||
CacheTime: 5,
|
// 已经选择了文件夹
|
||||||
})
|
var err error
|
||||||
return dispatcher.EndGroups
|
dir, err = database.GetDirByID(ctx, data.DirID)
|
||||||
|
if err != nil {
|
||||||
|
return failedAnswer("获取文件夹信息失败: " + err.Error())
|
||||||
|
}
|
||||||
|
user.DefaultDir = dir.ID
|
||||||
|
} else {
|
||||||
|
// 检查是否有可用的文件夹
|
||||||
|
dirs, err := database.GetDirsByUserIDAndStorageName(ctx, user.ID, storageName)
|
||||||
|
if err != nil {
|
||||||
|
return failedAnswer("获取目录失败: " + err.Error())
|
||||||
|
}
|
||||||
|
if len(dirs) > 0 {
|
||||||
|
// 要求选择文件夹
|
||||||
|
markup, err := msgelem.BuildSetDefaultDirMarkup(ctx, storageName, dirs)
|
||||||
|
if err != nil {
|
||||||
|
return failedAnswer("构建目录选择失败: " + err.Error())
|
||||||
|
}
|
||||||
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
|
Message: "请选择要保存到的默认文件夹",
|
||||||
|
ReplyMarkup: markup,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
}
|
}
|
||||||
user.DefaultStorage = selectedStorage.Name()
|
user.DefaultStorage = selectedStorage.Name()
|
||||||
if err := database.UpdateUser(ctx, user); err != nil {
|
if err := database.UpdateUser(ctx, user); err != nil {
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
return failedAnswer("更新用户信息失败: " + err.Error())
|
||||||
QueryID: update.CallbackQuery.GetQueryID(),
|
}
|
||||||
Alert: true,
|
msg := fmt.Sprintf("已将默认存储位置设为: %s", selectedStorage.Name())
|
||||||
Message: "更新用户信息失败: " + err.Error(),
|
if dir != nil {
|
||||||
CacheTime: 5,
|
msg += fmt.Sprintf(":/%s", strings.TrimPrefix(dir.Path, "/"))
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: update.CallbackQuery.GetMsgID(),
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
Message: "已将默认存储位置设置为: " + selectedStorage.Name(),
|
Message: msg,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -92,7 +113,7 @@ func handleStorageCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
markup, err := msgelem.BuildSetDefaultStorageMarkup(ctx, userID, storages)
|
markup, err := msgelem.BuildSetDefaultStorageMarkup(ctx, storages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取存储失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString("获取存储失败: "+err.Error()), nil)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
@@ -71,6 +73,10 @@ func handleSilentSaveTelegraph(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userID := update.GetUserChat().GetID()
|
userID := update.GetUserChat().GetID()
|
||||||
return shortcut.CreateAndAddtelegraphWithEdit(ctx, userID, result.Page, result.TphDir, result.Pics, stor, msg.ID)
|
dirpath := result.TphDir
|
||||||
|
if p := dirutil.PathFromContext(ctx); p != "" {
|
||||||
|
dirpath = path.Join(p, dirpath)
|
||||||
|
}
|
||||||
|
return shortcut.CreateAndAddtelegraphWithEdit(ctx, userID, result.Page, dirpath, result.Pics, stor, msg.ID)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
37
client/bot/handlers/utils/dirutil/context.go
Normal file
37
client/bot/handlers/utils/dirutil/context.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package dirutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey struct{}
|
||||||
|
|
||||||
|
var dirContextKey = contextKey{}
|
||||||
|
|
||||||
|
func WithContext(ctx context.Context, dir *database.Dir) context.Context {
|
||||||
|
if dir == nil {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, dirContextKey, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromContext(ctx context.Context) *database.Dir {
|
||||||
|
dir, ok := ctx.Value(dirContextKey).(*database.Dir)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathFromContext returns the directory path stored in the context.
|
||||||
|
//
|
||||||
|
// If no directory is found, an empty string is returned.
|
||||||
|
func PathFromContext(ctx context.Context) string {
|
||||||
|
dir := FromContext(ctx)
|
||||||
|
if dir == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return dir.Path
|
||||||
|
}
|
||||||
@@ -94,7 +94,10 @@ func BuildAddOneSelectStorageMessage(ctx context.Context, stors []storage.Storag
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildSetDefaultStorageMarkup(ctx context.Context, userID int64, stors []storage.Storage) (*tg.ReplyInlineMarkup, error) {
|
// Builds the inline keyboard for setting default storage
|
||||||
|
func BuildSetDefaultStorageMarkup(
|
||||||
|
ctx context.Context,
|
||||||
|
stors []storage.Storage) (*tg.ReplyInlineMarkup, error) {
|
||||||
buttons := make([]tg.KeyboardButtonClass, 0)
|
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||||
for _, storage := range stors {
|
for _, storage := range stors {
|
||||||
data := tcbdata.SetDefaultStorage{
|
data := tcbdata.SetDefaultStorage{
|
||||||
@@ -119,7 +122,35 @@ func BuildSetDefaultStorageMarkup(ctx context.Context, userID int64, stors []sto
|
|||||||
return markup, nil
|
return markup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildSetDirKeyboard(dirs []database.Dir, dataid string) (*tg.ReplyInlineMarkup, error) {
|
func BuildSetDefaultDirMarkup(ctx context.Context,
|
||||||
|
seletedStorage string,
|
||||||
|
dirs []database.Dir) (*tg.ReplyInlineMarkup, error) {
|
||||||
|
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||||
|
for _, dir := range dirs {
|
||||||
|
dataid := xid.New().String()
|
||||||
|
data := tcbdata.SetDefaultStorage{
|
||||||
|
StorageName: seletedStorage,
|
||||||
|
DirID: dir.ID,
|
||||||
|
}
|
||||||
|
err := cache.Set(dataid, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
||||||
|
Text: dir.Path,
|
||||||
|
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeSetDefault, dataid),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
markup := &tg.ReplyInlineMarkup{}
|
||||||
|
for i := 0; i < len(buttons); i += 3 {
|
||||||
|
row := tg.KeyboardButtonRow{}
|
||||||
|
row.Buttons = buttons[i:min(i+3, len(buttons))]
|
||||||
|
markup.Rows = append(markup.Rows, row)
|
||||||
|
}
|
||||||
|
return markup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildSetDirMarkupForAdd(dirs []database.Dir, dataid string) (*tg.ReplyInlineMarkup, error) {
|
||||||
data, ok := cache.Get[tcbdata.Add](dataid)
|
data, ok := cache.Get[tcbdata.Add](dataid)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("failed to get data from cache: %s", dataid)
|
return nil, fmt.Errorf("failed to get data from cache: %s", dataid)
|
||||||
|
|||||||
13
config/tg.go
13
config/tg.go
@@ -1,12 +1,13 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type telegramConfig struct {
|
type telegramConfig struct {
|
||||||
Token string `toml:"token" mapstructure:"token"`
|
Token string `toml:"token" mapstructure:"token"`
|
||||||
AppID int `toml:"app_id" mapstructure:"app_id" json:"app_id"`
|
AppID int `toml:"app_id" mapstructure:"app_id" json:"app_id"`
|
||||||
AppHash string `toml:"app_hash" mapstructure:"app_hash" json:"app_hash"`
|
AppHash string `toml:"app_hash" mapstructure:"app_hash" json:"app_hash"`
|
||||||
Proxy tgProxyConfig `toml:"proxy" mapstructure:"proxy"`
|
Proxy tgProxyConfig `toml:"proxy" mapstructure:"proxy"`
|
||||||
RpcRetry int `toml:"rpc_retry" mapstructure:"rpc_retry" json:"rpc_retry"`
|
RpcRetry int `toml:"rpc_retry" mapstructure:"rpc_retry" json:"rpc_retry"`
|
||||||
Userbot userbotConfig `toml:"userbot" mapstructure:"userbot" json:"userbot"`
|
Userbot userbotConfig `toml:"userbot" mapstructure:"userbot" json:"userbot"`
|
||||||
|
MediaGroupTimeout int `toml:"media_group_timeout" mapstructure:"media_group_timeout" json:"media_group_timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type userbotConfig struct {
|
type userbotConfig struct {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type User struct {
|
|||||||
ChatID int64 `gorm:"uniqueIndex;not null"`
|
ChatID int64 `gorm:"uniqueIndex;not null"`
|
||||||
Silent bool
|
Silent bool
|
||||||
DefaultStorage string
|
DefaultStorage string
|
||||||
|
DefaultDir uint // Dir.ID
|
||||||
Dirs []Dir
|
Dirs []Dir
|
||||||
ApplyRule bool
|
ApplyRule bool
|
||||||
Rules []Rule
|
Rules []Rule
|
||||||
|
|||||||
19
go.mod
19
go.mod
@@ -19,7 +19,8 @@ require (
|
|||||||
github.com/rs/xid v1.6.0
|
github.com/rs/xid v1.6.0
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
golang.org/x/net v0.46.0
|
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
|
||||||
|
golang.org/x/net v0.47.0
|
||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.14.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ require (
|
|||||||
github.com/segmentio/asm v1.2.1 // indirect
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
||||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
github.com/tetratelabs/wazero v1.10.1 // indirect
|
||||||
github.com/tinylib/msgp v1.4.0 // indirect
|
github.com/tinylib/msgp v1.4.0 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
@@ -97,7 +98,7 @@ require (
|
|||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.43.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
golang.org/x/oauth2 v0.32.0 // indirect
|
golang.org/x/oauth2 v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
@@ -118,8 +119,8 @@ require (
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.1 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/ncruces/go-sqlite3 v0.29.1
|
github.com/ncruces/go-sqlite3 v0.30.1
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.24.0
|
github.com/ncruces/go-sqlite3/gormlite v0.30.1
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
@@ -130,8 +131,8 @@ require (
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.uber.org/multierr v1.11.0
|
go.uber.org/multierr v1.11.0
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/sync v0.17.0
|
golang.org/x/sync v0.18.0
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/text v0.30.0
|
golang.org/x/text v0.31.0
|
||||||
gorm.io/gorm v1.31.0
|
gorm.io/gorm v1.31.1
|
||||||
)
|
)
|
||||||
|
|||||||
34
go.sum
34
go.sum
@@ -194,10 +194,10 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
|||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-sqlite3 v0.29.1 h1:NIi8AISWBToRHyoz01FXiTNvU147Tqdibgj2tFzJCqM=
|
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
|
||||||
github.com/ncruces/go-sqlite3 v0.29.1/go.mod h1:PpccBNNhvjwUOwDQEn2gXQPFPTWdlromj0+fSkd5KSg=
|
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.24.0 h1:81sHeq3CCdhjoqAB650n5wEdRlLO9VBvosArskcN3+c=
|
github.com/ncruces/go-sqlite3/gormlite v0.30.1 h1:kApjSKrepgmhtx63KMeD8aUoz1l4aJT4fkoBmHSsRns=
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.24.0/go.mod h1:vXfVWdBfg7qOgqQqHpzUWl9LLswD0h+8mK4oouaV2oc=
|
github.com/ncruces/go-sqlite3/gormlite v0.30.1/go.mod h1:zgFibXnnKek3qMHd/2A1OtfDqbN7ae+H80aMX+487As=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||||
@@ -256,8 +256,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
|
||||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
||||||
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
|
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
|
||||||
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
|
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
|
||||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
@@ -265,6 +265,8 @@ github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
|||||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c h1:xA2TJS9Hu/ivzaZIrDcwvpJ3Fnpsk5fDOJ4iSnL6J0w=
|
||||||
|
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
@@ -290,6 +292,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
@@ -307,6 +311,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||||
@@ -316,8 +322,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -330,8 +336,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -345,8 +351,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -371,8 +377,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
package parsers
|
package js
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/blang/semver"
|
"github.com/blang/semver"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/netutil"
|
"github.com/krau/SaveAny-Bot/common/utils/netutil"
|
||||||
"github.com/playwright-community/playwright-go"
|
"github.com/krau/SaveAny-Bot/parsers/parsers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func jsRegisterParser(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
|
func jsRegisterParser(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
|
||||||
@@ -57,7 +55,7 @@ func jsRegisterParser(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value
|
|||||||
if parseFn == nil || goja.IsUndefined(parseFn) {
|
if parseFn == nil || goja.IsUndefined(parseFn) {
|
||||||
return vm.NewGoError(errors.New("parser must provide a parse function"))
|
return vm.NewGoError(errors.New("parser must provide a parse function"))
|
||||||
}
|
}
|
||||||
AddParser(newJSParser(vm, handleFn, parseFn, metadata))
|
parsers.Add(newJSParser(vm, handleFn, parseFn, metadata))
|
||||||
return goja.Undefined()
|
return goja.Undefined()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,74 +171,3 @@ var jsGhttp = func(vm *goja.Runtime) *goja.Object {
|
|||||||
})
|
})
|
||||||
return ghttp
|
return ghttp
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsPlaywright = func(vm *goja.Runtime, logger *log.Logger) *goja.Object {
|
|
||||||
pwObj := vm.NewObject()
|
|
||||||
var installOnce sync.Once
|
|
||||||
slogger := slog.New(logger)
|
|
||||||
pwObj.Set("get", func(call goja.FunctionCall) goja.Value {
|
|
||||||
url := call.Argument(0).String()
|
|
||||||
var installErr error
|
|
||||||
installOnce.Do(func() {
|
|
||||||
installErr = playwright.Install(&playwright.RunOptions{
|
|
||||||
Browsers: []string{"chromium"},
|
|
||||||
DriverDirectory: "./playwright",
|
|
||||||
Logger: slogger,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if installErr != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("failed to install playwright: %v", installErr),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pw, err := playwright.Run(&playwright.RunOptions{
|
|
||||||
DriverDirectory: "./playwright",
|
|
||||||
Logger: slogger,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("failed to start playwright: %v", err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
defer pw.Stop()
|
|
||||||
|
|
||||||
browser, err := pw.Chromium.Launch()
|
|
||||||
if err != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("failed to launch browser: %v", err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
defer browser.Close()
|
|
||||||
|
|
||||||
page, err := browser.NewPage()
|
|
||||||
if err != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("failed to create page: %v", err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := page.Goto(url, playwright.PageGotoOptions{
|
|
||||||
WaitUntil: playwright.WaitUntilStateNetworkidle,
|
|
||||||
Timeout: playwright.Float(60000),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("failed to navigate: %v", err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if resp != nil && resp.Status() >= 400 {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("bad status code: %d", resp.Status()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
content, err := page.Content()
|
|
||||||
if err != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("failed to get page content: %v", err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return vm.ToValue(content)
|
|
||||||
})
|
|
||||||
return pwObj
|
|
||||||
}
|
|
||||||
82
parsers/js/api_playwright.go
Normal file
82
parsers/js/api_playwright.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package js
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/playwright-community/playwright-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jsPlaywright = func(vm *goja.Runtime, logger *log.Logger) *goja.Object {
|
||||||
|
pwObj := vm.NewObject()
|
||||||
|
var installOnce sync.Once
|
||||||
|
slogger := slog.New(logger)
|
||||||
|
pwObj.Set("get", func(call goja.FunctionCall) goja.Value {
|
||||||
|
url := call.Argument(0).String()
|
||||||
|
var installErr error
|
||||||
|
installOnce.Do(func() {
|
||||||
|
installErr = playwright.Install(&playwright.RunOptions{
|
||||||
|
Browsers: []string{"chromium"},
|
||||||
|
DriverDirectory: "./playwright",
|
||||||
|
Logger: slogger,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if installErr != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to install playwright: %v", installErr),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pw, err := playwright.Run(&playwright.RunOptions{
|
||||||
|
DriverDirectory: "./playwright",
|
||||||
|
Logger: slogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to start playwright: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
defer pw.Stop()
|
||||||
|
|
||||||
|
browser, err := pw.Chromium.Launch()
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to launch browser: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
defer browser.Close()
|
||||||
|
|
||||||
|
page, err := browser.NewPage()
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to create page: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := page.Goto(url, playwright.PageGotoOptions{
|
||||||
|
WaitUntil: playwright.WaitUntilStateNetworkidle,
|
||||||
|
Timeout: playwright.Float(60000),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to navigate: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if resp != nil && resp.Status() >= 400 {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("bad status code: %d", resp.Status()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
content, err := page.Content()
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to get page content: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return vm.ToValue(content)
|
||||||
|
})
|
||||||
|
return pwObj
|
||||||
|
}
|
||||||
19
parsers/js/api_playwright_stub.go
Normal file
19
parsers/js/api_playwright_stub.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build no_playwright
|
||||||
|
|
||||||
|
package js
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jsPlaywright = func(vm *goja.Runtime, _ *log.Logger) *goja.Object {
|
||||||
|
pwObj := vm.NewObject()
|
||||||
|
unsupported := vm.ToValue(map[string]any{
|
||||||
|
"error": "playwright is not supported in this build",
|
||||||
|
})
|
||||||
|
pwObj.Set("get", func(call goja.FunctionCall) goja.Value {
|
||||||
|
return unsupported
|
||||||
|
})
|
||||||
|
return pwObj
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package parsers
|
package js
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package parsers
|
package js
|
||||||
|
|
||||||
import "github.com/blang/semver"
|
import "github.com/blang/semver"
|
||||||
|
|
||||||
@@ -3,41 +3,16 @@ package parsers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/parsers/js"
|
||||||
"github.com/krau/SaveAny-Bot/parsers/kemono"
|
"github.com/krau/SaveAny-Bot/parsers/native/kemono"
|
||||||
"github.com/krau/SaveAny-Bot/parsers/twitter"
|
"github.com/krau/SaveAny-Bot/parsers/native/twitter"
|
||||||
|
"github.com/krau/SaveAny-Bot/parsers/parsers"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/parser"
|
"github.com/krau/SaveAny-Bot/pkg/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
parsers []parser.Parser
|
|
||||||
parsersMu sync.Mutex
|
|
||||||
doConfig sync.Once
|
|
||||||
configParsers = func() {
|
|
||||||
if len(parsers) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, pser := range parsers {
|
|
||||||
if configurable, ok := pser.(parser.ConfigurableParser); ok {
|
|
||||||
cfg := config.C().GetParserConfigByName(configurable.Name())
|
|
||||||
if err := configurable.Configure(cfg); err != nil {
|
|
||||||
fmt.Printf("Error configuring parser %s: %v\n", configurable.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func AddParser(p ...parser.Parser) {
|
|
||||||
parsersMu.Lock()
|
|
||||||
defer parsersMu.Unlock()
|
|
||||||
parsers = append(parsers, p...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
AddParser(new(twitter.TwitterParser), new(kemono.KemonoParser))
|
parsers.Add(new(twitter.TwitterParser), new(kemono.KemonoParser))
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -45,12 +20,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ParseWithContext(ctx context.Context, url string) (*parser.Item, error) {
|
func ParseWithContext(ctx context.Context, url string) (*parser.Item, error) {
|
||||||
doConfig.Do(configParsers)
|
|
||||||
ch := make(chan *parser.Item, 1)
|
ch := make(chan *parser.Item, 1)
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for _, pser := range parsers {
|
for _, pser := range parsers.Get() {
|
||||||
if !pser.CanHandle(url) {
|
if !pser.CanHandle(url) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -76,11 +50,18 @@ func ParseWithContext(ctx context.Context, url string) (*parser.Item, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CanHandle(url string) (bool, parser.Parser) {
|
func CanHandle(url string) (bool, parser.Parser) {
|
||||||
doConfig.Do(configParsers)
|
for _, pser := range parsers.Get() {
|
||||||
for _, pser := range parsers {
|
|
||||||
if pser.CanHandle(url) {
|
if pser.CanHandle(url) {
|
||||||
return true, pser
|
return true, pser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoadPlugins(ctx context.Context, dir string) error {
|
||||||
|
return js.LoadPlugins(ctx, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddPlugin(ctx context.Context, code string, name string) error {
|
||||||
|
return js.AddPlugin(ctx, code, name)
|
||||||
|
}
|
||||||
|
|||||||
43
parsers/parsers/parsers.go
Normal file
43
parsers/parsers/parsers.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
parsers []parser.Parser
|
||||||
|
mu sync.Mutex
|
||||||
|
configOnce sync.Once
|
||||||
|
configParsers = func() {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if len(parsers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, pser := range parsers {
|
||||||
|
if configurable, ok := pser.(parser.ConfigurableParser); ok {
|
||||||
|
cfg := config.C().GetParserConfigByName(configurable.Name())
|
||||||
|
if err := configurable.Configure(cfg); err != nil {
|
||||||
|
fmt.Printf("Error configuring parser %s: %v\n", configurable.Name(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Add(p ...parser.Parser) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
parsers = append(parsers, p...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get() []parser.Parser {
|
||||||
|
configOnce.Do(configParsers)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
return parsers
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
TypeAdd = "add"
|
TypeAdd = "add"
|
||||||
TypeSetDefault = "setdefault"
|
TypeSetDefault = "setdefault"
|
||||||
TypeConfig = "config"
|
TypeConfig = "config"
|
||||||
TypeCancel = "cancel"
|
TypeCancel = "cancel"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,4 +47,5 @@ type Add struct {
|
|||||||
|
|
||||||
type SetDefaultStorage struct {
|
type SetDefaultStorage struct {
|
||||||
StorageName string
|
StorageName string
|
||||||
|
DirID uint
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ func (t *Telegram) Save(ctx context.Context, r io.Reader, storagePath string) er
|
|||||||
if err := t.limiter.Wait(ctx); err != nil {
|
if err := t.limiter.Wait(ctx); err != nil {
|
||||||
return fmt.Errorf("rate limit failed: %w", err)
|
return fmt.Errorf("rate limit failed: %w", err)
|
||||||
}
|
}
|
||||||
rs, ok := r.(io.ReadSeeker)
|
rs, seekable := r.(io.ReadSeeker)
|
||||||
if !ok || rs == nil {
|
if !seekable || rs == nil {
|
||||||
return fmt.Errorf("reader must implement io.ReadSeeker")
|
return fmt.Errorf("reader must implement io.ReadSeeker")
|
||||||
}
|
}
|
||||||
tctx := tgutil.ExtFromContext(ctx)
|
tctx := tgutil.ExtFromContext(ctx)
|
||||||
@@ -147,6 +147,17 @@ func (t *Telegram) Save(ctx context.Context, r io.Reader, storagePath string) er
|
|||||||
switch mtypeStr := mtype.String(); {
|
switch mtypeStr := mtype.String(); {
|
||||||
case strings.HasPrefix(mtypeStr, "video/"):
|
case strings.HasPrefix(mtypeStr, "video/"):
|
||||||
media = docb.Video().SupportsStreaming()
|
media = docb.Video().SupportsStreaming()
|
||||||
|
rs.Seek(0, io.SeekStart)
|
||||||
|
switch mtypeStr {
|
||||||
|
case "video/mp4":
|
||||||
|
info, err := getMP4Info(rs)
|
||||||
|
if err == nil {
|
||||||
|
media = docb.Video().
|
||||||
|
Duration(time.Duration(info.Duration)*time.Second).
|
||||||
|
Resolution(info.Width, info.Height).
|
||||||
|
SupportsStreaming()
|
||||||
|
}
|
||||||
|
}
|
||||||
case strings.HasPrefix(mtypeStr, "audio/"):
|
case strings.HasPrefix(mtypeStr, "audio/"):
|
||||||
media = docb.Audio().Title(filename)
|
media = docb.Audio().Title(filename)
|
||||||
case strings.HasPrefix(mtypeStr, "image/") && !strings.HasSuffix(mtypeStr, "webp"):
|
case strings.HasPrefix(mtypeStr, "image/") && !strings.HasSuffix(mtypeStr, "webp"):
|
||||||
|
|||||||
36
storage/telegram/util.go
Normal file
36
storage/telegram/util.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/yapingcat/gomedia/go-mp4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MP4Info struct {
|
||||||
|
Duration int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMP4Info(r io.ReadSeeker) (*MP4Info, error) {
|
||||||
|
d := mp4.CreateMp4Demuxer(r)
|
||||||
|
|
||||||
|
tracks, err := d.ReadHead()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, track := range tracks {
|
||||||
|
if track.Cid == mp4.MP4_CODEC_H264 {
|
||||||
|
info := d.GetMp4Info()
|
||||||
|
return &MP4Info{
|
||||||
|
Duration: int(info.Duration / info.Timescale),
|
||||||
|
Width: int(track.Width),
|
||||||
|
Height: int(track.Height),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no h264 track found")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user