Compare commits

..

2 Commits

Author SHA1 Message Date
krau
131dfeb4cd refactor: js plugin api 2025-11-16 21:38:30 +08:00
krau
3f40acff55 feat: add directory management functionality and update handlers to utilize default directory 2025-11-16 21:09:55 +08:00
26 changed files with 313 additions and 169 deletions

View File

@@ -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()))

View File

@@ -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)
} }

View File

@@ -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"
@@ -32,12 +33,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,18 +69,12 @@ 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 { type MediaGroupHandler struct {

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
} }

View 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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View 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
}

View 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
}

View File

@@ -1,4 +1,4 @@
package parsers package js
import ( import (
"context" "context"

View File

@@ -1,4 +1,4 @@
package parsers package js
import "github.com/blang/semver" import "github.com/blang/semver"

View File

@@ -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)
}

View File

@@ -0,0 +1,44 @@
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) {
configOnce.Do(configParsers)
mu.Lock()
defer mu.Unlock()
parsers = append(parsers, p...)
}
func Get() []parser.Parser {
configOnce.Do(configParsers)
mu.Lock()
defer mu.Unlock()
return parsers
}

View File

@@ -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
} }