mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-05-10 17:52:44 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d837e946c | ||
|
|
f947ee6fc7 | ||
|
|
40ad12a892 | ||
|
|
697e419643 | ||
|
|
eef051de3b | ||
|
|
6e29442c05 | ||
|
|
a3f1f75caf | ||
|
|
f05dd883e3 | ||
|
|
9cb866de8c | ||
|
|
980455fd24 | ||
|
|
24978470cd | ||
|
|
215e082028 | ||
|
|
a7b93e57fc | ||
|
|
a4b3b459a9 |
@@ -19,7 +19,7 @@ import (
|
|||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(ctx context.Context) (<-chan struct{}) {
|
func Init(ctx context.Context) <-chan struct{} {
|
||||||
log.FromContext(ctx).Info("初始化 Bot...")
|
log.FromContext(ctx).Info("初始化 Bot...")
|
||||||
resultChan := make(chan struct {
|
resultChan := make(chan struct {
|
||||||
client *gotgproto.Client
|
client *gotgproto.Client
|
||||||
@@ -75,18 +75,9 @@ func Init(ctx context.Context) (<-chan struct{}) {
|
|||||||
client.API().BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{
|
client.API().BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{
|
||||||
Scope: &tg.BotCommandScopeDefault{},
|
Scope: &tg.BotCommandScopeDefault{},
|
||||||
})
|
})
|
||||||
commands := []tg.BotCommand{
|
commands := make([]tg.BotCommand, 0, len(handlers.CommandHandlers))
|
||||||
{Command: "start", Description: "开始使用"},
|
for _, info := range handlers.CommandHandlers {
|
||||||
{Command: "help", Description: "显示帮助"},
|
commands = append(commands, tg.BotCommand{Command: info.Cmd, Description: info.Desc})
|
||||||
{Command: "silent", Description: "开启/关闭静默模式"},
|
|
||||||
{Command: "storage", Description: "设置默认存储端"},
|
|
||||||
{Command: "save", Description: "保存文件"},
|
|
||||||
{Command: "dir", Description: "管理存储文件夹"},
|
|
||||||
{Command: "rule", Description: "管理规则"},
|
|
||||||
}
|
|
||||||
if config.C().Telegram.Userbot.Enable {
|
|
||||||
commands = append(commands, tg.BotCommand{Command: "watch", Description: "监听聊天"})
|
|
||||||
commands = append(commands, tg.BotCommand{Command: "unwatch", Description: "取消监听聊天"})
|
|
||||||
}
|
}
|
||||||
_, err = client.API().BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{
|
_, err = client.API().BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{
|
||||||
Scope: &tg.BotCommandScopeDefault{},
|
Scope: &tg.BotCommandScopeDefault{},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
@@ -101,3 +102,43 @@ func handleConfigFnameSTCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleConfigFnameTmpl(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
userID := update.GetUserChat().GetID()
|
||||||
|
user, err := database.GetUserByChatID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
args := strings.Fields(string(update.EffectiveMessage.Text))
|
||||||
|
if len(args) <= 1 {
|
||||||
|
text := `使用该命令设置文件名模板, 示例:
|
||||||
|
/fnametmpl 图片_{{.msgid}}_{{.msgdate}}.jpg
|
||||||
|
|
||||||
|
可用变量:
|
||||||
|
- {{.msgid}}: 消息ID
|
||||||
|
- {{.msgtags}}: 消息中的标签, 将以下划线分隔输出
|
||||||
|
- {{.msggen}}: 根据消息生成的文件名
|
||||||
|
- {{.msgdate}}: 消息日期, 格式 YYYY-MM-DD_HH-MM-SS
|
||||||
|
- {{.origname}}: 媒体的原始文件名 (如果有)
|
||||||
|
- {{.chatid}}: 消息的聊天ID
|
||||||
|
`
|
||||||
|
if user.FilenameTemplate != "" {
|
||||||
|
text += fmt.Sprintf("\n\n当前模板: %s", user.FilenameTemplate)
|
||||||
|
}
|
||||||
|
text += "\n\n模板仅在文件名策略设置为 '自定义模板' 时生效, 且模板解析错误时会回退到默认文件名"
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(text), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
newTmpl := strings.Join(args[1:], " ")
|
||||||
|
_, err = template.New("filename").Parse(newTmpl)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无效的模板, 请检查语法\n"+err.Error()), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
user.FilenameTemplate = newTmpl
|
||||||
|
if err := database.UpdateUser(ctx, user); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("已更新文件名模板"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,31 +5,16 @@ import (
|
|||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleHelpCmd(ctx *ext.Context, update *ext.Update) error {
|
func handleHelpCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
const helpText string = `
|
|
||||||
Save Any Bot - 转存你的 Telegram 文件
|
|
||||||
版本: %s , 提交: %s
|
|
||||||
|
|
||||||
命令:
|
|
||||||
/start - 开始使用
|
|
||||||
/help - 显示帮助
|
|
||||||
/silent - 开关静默模式
|
|
||||||
/storage - 设置默认存储位置
|
|
||||||
/save [自定义文件名] - 保存文件
|
|
||||||
/dir - 管理存储目录
|
|
||||||
/rule - 管理规则
|
|
||||||
/update - 检查更新并升级
|
|
||||||
|
|
||||||
使用帮助: https://sabot.unv.app/usage
|
|
||||||
反馈群组: https://t.me/ProjectSaveAny
|
|
||||||
`
|
|
||||||
shortHash := config.GitCommit
|
shortHash := config.GitCommit
|
||||||
if len(shortHash) > 7 {
|
if len(shortHash) > 7 {
|
||||||
shortHash = shortHash[:7]
|
shortHash = shortHash[:7]
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf(helpText, config.Version, shortHash)), nil)
|
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf(i18n.T(i18nk.BotMsgHelpTextFmt), config.Version, shortHash)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"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/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/fnamest"
|
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
@@ -33,12 +32,13 @@ func handleMediaMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tfOpts := make([]tfile.TGFileOption, 0)
|
// tfOpts := make([]tfile.TGFileOption, 0)
|
||||||
switch userDB.FilenameStrategy {
|
// switch userDB.FilenameStrategy {
|
||||||
case fnamest.Message.String():
|
// case fnamest.Message.String():
|
||||||
tfOpts = append(tfOpts, tfile.WithName(tgutil.GenFileNameFromMessage(*message)))
|
// tfOpts = append(tfOpts, tfile.WithName(tgutil.GenFileNameFromMessage(*message)))
|
||||||
default:
|
// default:
|
||||||
}
|
// }
|
||||||
|
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
|
||||||
@@ -74,12 +74,13 @@ func handleSilentSaveMedia(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tfOpts := make([]tfile.TGFileOption, 0)
|
// tfOpts := make([]tfile.TGFileOption, 0)
|
||||||
switch userDB.FilenameStrategy {
|
// switch userDB.FilenameStrategy {
|
||||||
case fnamest.Message.String():
|
// case fnamest.Message.String():
|
||||||
tfOpts = append(tfOpts, tfile.WithName(tgutil.GenFileNameFromMessage(*message)))
|
// tfOpts = append(tfOpts, tfile.WithName(tgutil.GenFileNameFromMessage(*message)))
|
||||||
default:
|
// default:
|
||||||
}
|
// }
|
||||||
|
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
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"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"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/parsers"
|
"github.com/krau/SaveAny-Bot/parsers"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
@@ -21,6 +23,10 @@ import (
|
|||||||
func handleTextMessage(ctx *ext.Context, u *ext.Update) error {
|
func handleTextMessage(ctx *ext.Context, u *ext.Update) error {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
text := u.EffectiveMessage.Text
|
text := u.EffectiveMessage.Text
|
||||||
|
entityUrls := tgutil.ExtractMessageEntityUrls(u.EffectiveMessage.Message)
|
||||||
|
if len(entityUrls) > 0 {
|
||||||
|
text += "\n" + strings.Join(entityUrls, "\n")
|
||||||
|
}
|
||||||
ok, pser := parsers.CanHandle(text)
|
ok, pser := parsers.CanHandle(text)
|
||||||
if !ok {
|
if !ok {
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
|
|||||||
@@ -1,28 +1,40 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/dispatcher/handlers"
|
"github.com/celestix/gotgproto/dispatcher/handlers"
|
||||||
"github.com/celestix/gotgproto/dispatcher/handlers/filters"
|
"github.com/celestix/gotgproto/dispatcher/handlers/filters"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
sabotfilters "github.com/krau/SaveAny-Bot/client/bot/handlers/utils/filters"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/re"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/re"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/ruleutil"
|
|
||||||
userclient "github.com/krau/SaveAny-Bot/client/user"
|
userclient "github.com/krau/SaveAny-Bot/client/user"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/core"
|
|
||||||
"github.com/krau/SaveAny-Bot/core/tasks/tfile"
|
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
|
||||||
"github.com/rs/xid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DescCommandHandler struct {
|
||||||
|
Cmd string
|
||||||
|
Desc string
|
||||||
|
handler func(ctx *ext.Context, u *ext.Update) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var CommandHandlers = []DescCommandHandler{
|
||||||
|
{"start", "开始使用", handleHelpCmd},
|
||||||
|
{"silent", "切换静默模式", handleSilentCmd},
|
||||||
|
{"storage", "设置默认存储端", handleStorageCmd},
|
||||||
|
{"dir", "管理存储文件夹", handleDirCmd},
|
||||||
|
{"rule", "管理自动存储规则", handleRuleCmd},
|
||||||
|
{"watch", "监听聊天(UserBot)", handleWatchCmd},
|
||||||
|
{"unwatch", "取消监听聊天(UserBot)", handleUnwatchCmd},
|
||||||
|
{"save", "保存文件", handleSilentMode(handleSaveCmd, handleSilentSaveReplied)},
|
||||||
|
{"config", "修改配置", handleConfigCmd},
|
||||||
|
{"fnametmpl", "设置文件命名模板", handleConfigFnameTmpl},
|
||||||
|
{"update", "检查更新", handleUpdateCmd},
|
||||||
|
{"help", "显示帮助", handleHelpCmd},
|
||||||
|
}
|
||||||
|
|
||||||
func Register(disp dispatcher.Dispatcher) {
|
func Register(disp dispatcher.Dispatcher) {
|
||||||
disp.AddHandler(handlers.NewMessage(filters.Message.ChatType(filters.ChatTypeChannel), func(ctx *ext.Context, u *ext.Update) error {
|
disp.AddHandler(handlers.NewMessage(filters.Message.ChatType(filters.ChatTypeChannel), func(ctx *ext.Context, u *ext.Update) error {
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
@@ -31,32 +43,16 @@ func Register(disp dispatcher.Dispatcher) {
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}))
|
}))
|
||||||
disp.AddHandler(handlers.NewMessage(filters.Message.All, checkPermission))
|
disp.AddHandler(handlers.NewMessage(filters.Message.All, checkPermission))
|
||||||
disp.AddHandler(handlers.NewCommand("start", handleHelpCmd))
|
for _, info := range CommandHandlers {
|
||||||
disp.AddHandler(handlers.NewCommand("help", handleHelpCmd))
|
disp.AddHandler(handlers.NewCommand(info.Cmd, info.handler))
|
||||||
disp.AddHandler(handlers.NewCommand("silent", handleSilentCmd))
|
}
|
||||||
disp.AddHandler(handlers.NewCommand("storage", handleStorageCmd))
|
|
||||||
disp.AddHandler(handlers.NewCommand("dir", handleDirCmd))
|
|
||||||
disp.AddHandler(handlers.NewCommand("rule", handleRuleCmd))
|
|
||||||
disp.AddHandler(handlers.NewCommand("watch", handleWatchCmd))
|
|
||||||
disp.AddHandler(handlers.NewCommand("unwatch", handleUnwatchCmd))
|
|
||||||
disp.AddHandler(handlers.NewCommand("save", handleSilentMode(handleSaveCmd, handleSilentSaveReplied)))
|
|
||||||
disp.AddHandler(handlers.NewCommand("config", handleConfigCmd))
|
|
||||||
disp.AddHandler(handlers.NewCommand("update", handleUpdateCmd))
|
|
||||||
disp.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("update"), handleUpdateCallback))
|
disp.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("update"), handleUpdateCallback))
|
||||||
disp.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix(tcbdata.TypeAdd), handleAddCallback))
|
disp.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix(tcbdata.TypeAdd), handleAddCallback))
|
||||||
disp.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix(tcbdata.TypeSetDefault), handleSetDefaultCallback))
|
disp.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix(tcbdata.TypeSetDefault), handleSetDefaultCallback))
|
||||||
disp.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix(tcbdata.TypeCancel), handleCancelCallback))
|
disp.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix(tcbdata.TypeCancel), handleCancelCallback))
|
||||||
disp.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix(tcbdata.TypeConfig), handleConfigCallback))
|
disp.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix(tcbdata.TypeConfig), handleConfigCallback))
|
||||||
linkRegexFilter, err := filters.Message.Regex(re.TgMessageLinkRegexString)
|
disp.AddHandler(handlers.NewMessage(sabotfilters.RegexUrl(regexp.MustCompile(re.TgMessageLinkRegexString)), handleSilentMode(handleMessageLink, handleSilentSaveLink)))
|
||||||
if err != nil {
|
disp.AddHandler(handlers.NewMessage(sabotfilters.RegexUrl(regexp.MustCompile(re.TelegraphUrlRegexString)), handleSilentMode(handleTelegraphUrlMessage, handleSilentSaveTelegraph)))
|
||||||
panic("failed to create regex filter: " + err.Error())
|
|
||||||
}
|
|
||||||
disp.AddHandler(handlers.NewMessage(linkRegexFilter, handleSilentMode(handleMessageLink, handleSilentSaveLink)))
|
|
||||||
telegraphUrlRegexFilter, err := filters.Message.Regex(re.TelegraphUrlRegexString)
|
|
||||||
if err != nil {
|
|
||||||
panic("failed to create Telegraph URL regex filter: " + err.Error())
|
|
||||||
}
|
|
||||||
disp.AddHandler(handlers.NewMessage(telegraphUrlRegexFilter, handleSilentMode(handleTelegraphUrlMessage, handleSilentSaveTelegraph)))
|
|
||||||
disp.AddHandler(handlers.NewMessage(filters.Message.Media, handleSilentMode(handleMediaMessage, handleSilentSaveMedia)))
|
disp.AddHandler(handlers.NewMessage(filters.Message.Media, handleSilentMode(handleMediaMessage, handleSilentSaveMedia)))
|
||||||
disp.AddHandler(handlers.NewMessage(filters.Message.Text, handleSilentMode(handleTextMessage, handleSilentSaveText)))
|
disp.AddHandler(handlers.NewMessage(filters.Message.Text, handleSilentMode(handleTextMessage, handleSilentSaveText)))
|
||||||
|
|
||||||
@@ -64,83 +60,3 @@ func Register(disp dispatcher.Dispatcher) {
|
|||||||
go listenMediaMessageEvent(userclient.GetMediaMessageCh())
|
go listenMediaMessageEvent(userclient.GetMediaMessageCh())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
|
|
||||||
logger := log.FromContext(userclient.GetCtx())
|
|
||||||
for event := range ch {
|
|
||||||
logger.Debug("Received media message event", "chat_id", event.ChatID, "file_name", event.File.Name())
|
|
||||||
ctx := event.Ctx
|
|
||||||
file := event.File
|
|
||||||
chats, err := database.GetWatchChatsByChatID(ctx, event.ChatID)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to get watch chats for chat ID %d: %v", event.ChatID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
msgText := event.File.Message().GetMessage()
|
|
||||||
for _, chat := range chats {
|
|
||||||
if chat.Filter != "" {
|
|
||||||
filter := strings.Split(chat.Filter, ":")
|
|
||||||
if len(filter) != 2 {
|
|
||||||
logger.Warnf("Invalid filter format in chat %d, skipping", chat.ChatID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filterType := filter[0]
|
|
||||||
filterData := filter[1]
|
|
||||||
switch filterType {
|
|
||||||
case "msgre": // [TODO] enums for filter types
|
|
||||||
if ok, err := regexp.MatchString(filterData, msgText); err != nil {
|
|
||||||
continue
|
|
||||||
} else if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
logger.Warnf("Unsupported filter type %s in chat %d, skipping", filterType, chat.ChatID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
user, err := database.GetUserByID(ctx, chat.UserID)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to get user by ID %d: %v", chat.UserID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if user.DefaultStorage == "" {
|
|
||||||
logger.Warnf("User %d has no default storage set, skipping media message handling", chat.UserID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stor, err := storage.GetStorageByUserIDAndName(ctx, user.ChatID, user.DefaultStorage)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to get storage by user ID %d and name %s: %v", user.ChatID, user.DefaultStorage, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var dirPath string
|
|
||||||
if user.ApplyRule && user.Rules != nil {
|
|
||||||
matched, matchedStorageName, matchedDirPath := ruleutil.ApplyRule(ctx, user.Rules, ruleutil.NewInput(file))
|
|
||||||
if !matched {
|
|
||||||
goto startCreateTask
|
|
||||||
}
|
|
||||||
dirPath = matchedDirPath.String()
|
|
||||||
if matchedStorageName.IsUsable() {
|
|
||||||
stor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, matchedStorageName.String())
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Failed to get storage by user ID and name: %s", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startCreateTask:
|
|
||||||
storagePath := stor.JoinStoragePath(path.Join(dirPath, file.Name()))
|
|
||||||
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
|
|
||||||
taskid := xid.New().String()
|
|
||||||
task, err := tfile.NewTGFileTask(taskid, injectCtx, file, stor, storagePath, nil)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("create task failed: %s", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := core.AddTask(injectCtx, task); err != nil {
|
|
||||||
logger.Errorf("add task failed: %s", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
logger.Infof("Added media message task for user %d in chat %d: %s", chat.UserID, event.ChatID, file.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ import (
|
|||||||
"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/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/strutil"
|
"github.com/krau/SaveAny-Bot/common/utils/strutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
|
|
||||||
@@ -28,21 +31,30 @@ func handleSaveCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
replyTo := update.EffectiveMessage.ReplyToMessage
|
replyTo := update.EffectiveMessage.ReplyToMessage
|
||||||
if replyTo == nil || replyTo.Message == nil {
|
if replyTo == nil || replyTo.Message == nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString(msgelem.SaveHelpText), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgSaveHelpText)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
genFilename := func() string {
|
// genFilename := func() string {
|
||||||
if len(args) > 1 {
|
// if len(args) > 1 {
|
||||||
return args[1]
|
// return args[1]
|
||||||
}
|
// }
|
||||||
filename := tgutil.GenFileNameFromMessage(*replyTo.Message)
|
// filename := tgutil.GenFileNameFromMessage(*replyTo.Message)
|
||||||
return filename
|
// return filename
|
||||||
}()
|
// }()
|
||||||
option := tfile.WithNameIfEmpty(genFilename)
|
// option := tfile.WithNameIfEmpty(genFilename)
|
||||||
if len(args) > 1 {
|
// if len(args) > 1 {
|
||||||
option = tfile.WithName(genFilename)
|
// option = tfile.WithName(genFilename)
|
||||||
|
// }
|
||||||
|
userDB, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, replyTo.Message, option)
|
opts := mediautil.TfileOptions(ctx, userDB, replyTo.Message)
|
||||||
|
if len(args) > 1 {
|
||||||
|
// custom filename via command arg
|
||||||
|
opts = append(opts, tfile.WithName(strings.Join(args[1:], " ")))
|
||||||
|
}
|
||||||
|
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, replyTo.Message, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -72,21 +84,30 @@ func handleSilentSaveReplied(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
replyTo := update.EffectiveMessage.ReplyToMessage
|
replyTo := update.EffectiveMessage.ReplyToMessage
|
||||||
if replyTo == nil || replyTo.Message == nil {
|
if replyTo == nil || replyTo.Message == nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString(msgelem.SaveHelpText), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgSaveHelpText)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
genFilename := func() string {
|
// genFilename := func() string {
|
||||||
if len(args) > 1 {
|
// if len(args) > 1 {
|
||||||
return args[1]
|
// return args[1]
|
||||||
}
|
// }
|
||||||
filename := tgutil.GenFileNameFromMessage(*replyTo.Message)
|
// filename := tgutil.GenFileNameFromMessage(*replyTo.Message)
|
||||||
return filename
|
// return filename
|
||||||
}()
|
// }()
|
||||||
option := tfile.WithNameIfEmpty(genFilename)
|
// option := tfile.WithNameIfEmpty(genFilename)
|
||||||
if len(args) > 1 {
|
// if len(args) > 1 {
|
||||||
option = tfile.WithName(genFilename)
|
// option = tfile.WithName(genFilename)
|
||||||
|
// }
|
||||||
|
userDB, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, replyTo.Message, option)
|
opts := mediautil.TfileOptions(ctx, userDB, replyTo.Message)
|
||||||
|
if len(args) > 1 {
|
||||||
|
// custom filename via command arg
|
||||||
|
opts = append(opts, tfile.WithName(strings.Join(args[1:], " ")))
|
||||||
|
}
|
||||||
|
msg, file, err := shortcut.GetFileFromMessageWithReply(ctx, update, replyTo.Message, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -124,7 +145,7 @@ func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: generator istead of get all messages
|
// [TODO]: generator istead of get all messages
|
||||||
msgs, err := tgutil.GetMessagesRange(ctx, chatID, int(startID), int(endID))
|
msgs, err := tgutil.GetMessagesRange(ctx, chatID, int(startID), int(endID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取消息失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString("获取消息失败: "+err.Error()), nil)
|
||||||
|
|||||||
26
client/bot/handlers/utils/filters/url.go
Normal file
26
client/bot/handlers/utils/filters/url.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package filters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher/handlers/filters"
|
||||||
|
"github.com/celestix/gotgproto/types"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegexUrl(r *regexp.Regexp) filters.MessageFilter {
|
||||||
|
return func(m *types.Message) bool {
|
||||||
|
if m.Text == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.MatchString(m.Text) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
urls := tgutil.ExtractMessageEntityUrls(m.Message)
|
||||||
|
if len(urls) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return slices.ContainsFunc(urls, r.MatchString)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,20 @@
|
|||||||
package mediautil
|
package mediautil
|
||||||
|
|
||||||
import "github.com/gotd/td/tg"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/strutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/fnamest"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
|
)
|
||||||
|
|
||||||
func IsSupported(media tg.MessageMediaClass) bool {
|
func IsSupported(media tg.MessageMediaClass) bool {
|
||||||
switch media.(type) {
|
switch media.(type) {
|
||||||
@@ -10,3 +24,119 @@ func IsSupported(media tg.MessageMediaClass) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FilenameTemplateData struct {
|
||||||
|
MsgID string `json:"msgid,omitempty"`
|
||||||
|
MsgTags string `json:"msgtags,omitempty"`
|
||||||
|
MsgGen string `json:"msggen,omitempty"`
|
||||||
|
MsgDate string `json:"msgdate,omitempty"`
|
||||||
|
OrigName string `json:"origname,omitempty"`
|
||||||
|
ChatID string `json:"chatid,omitempty"`
|
||||||
|
ChatTitle string `json:"chattitle,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FilenameTemplateData) ToMap() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"msgid": f.MsgID,
|
||||||
|
"msgtags": f.MsgTags,
|
||||||
|
"msggen": f.MsgGen,
|
||||||
|
"msgdate": f.MsgDate,
|
||||||
|
"origname": f.OrigName,
|
||||||
|
"chatid": f.ChatID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TfileOptions(ctx context.Context, user *database.User, message *tg.Message) []tfile.TGFileOption {
|
||||||
|
opts := make([]tfile.TGFileOption, 0)
|
||||||
|
var fnameOpt tfile.TGFileOption
|
||||||
|
switch user.FilenameStrategy {
|
||||||
|
case fnamest.Message.String():
|
||||||
|
fnameOpt = tfile.WithName(tgutil.GenFileNameFromMessage(*message))
|
||||||
|
case fnamest.Template.String():
|
||||||
|
if user.FilenameTemplate == "" {
|
||||||
|
log.FromContext(ctx).Warnf("empty filename template")
|
||||||
|
fnameOpt = tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*message))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tmpl, err := template.New("filename").Parse(user.FilenameTemplate)
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Errorf("failed to parse filename template: %s", err)
|
||||||
|
fnameOpt = tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*message))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
data := BuildFilenameTemplateData(message)
|
||||||
|
var sb strings.Builder
|
||||||
|
err = tmpl.Execute(&sb, data)
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Errorf("failed to execute filename template: %s", err)
|
||||||
|
fnameOpt = tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*message))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fnameOpt = tfile.WithName(sb.String())
|
||||||
|
default:
|
||||||
|
fnameOpt = tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*message))
|
||||||
|
}
|
||||||
|
opts = append(opts, fnameOpt, tfile.WithMessage(message))
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildFilenameTemplateData(message *tg.Message) map[string]string {
|
||||||
|
data := FilenameTemplateData{
|
||||||
|
MsgID: func() string {
|
||||||
|
id := message.GetID()
|
||||||
|
if id == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", id)
|
||||||
|
}(),
|
||||||
|
MsgTags: func() string {
|
||||||
|
tags := strutil.ExtractTagsFromText(message.GetMessage())
|
||||||
|
if len(tags) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join(tags, "_")
|
||||||
|
}(),
|
||||||
|
MsgGen: tgutil.GenFileNameFromMessage(*message),
|
||||||
|
OrigName: func() string {
|
||||||
|
f, _ := tgutil.GetMediaFileName(message.Media)
|
||||||
|
return f
|
||||||
|
}(),
|
||||||
|
MsgDate: func() string {
|
||||||
|
date := message.GetDate()
|
||||||
|
if date == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
t := time.Unix(int64(date), 0)
|
||||||
|
return t.Format("2006-01-02_15-04-05")
|
||||||
|
}(),
|
||||||
|
ChatID: func() string {
|
||||||
|
// 如果消息是频道的(从消息链接中fetch的) 直接使用其chat id, 无论它是否是从其他来源转发的
|
||||||
|
if message.GetPost() {
|
||||||
|
peer := message.GetPeerID()
|
||||||
|
switch p := peer.(type) {
|
||||||
|
case *tg.PeerChannel:
|
||||||
|
return intToStringOmitZero(p.ChannelID)
|
||||||
|
default: // impossible case
|
||||||
|
return intToStringOmitZero(tgutil.ChatIdFromPeer(peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fwdHeader, ok := message.GetFwdFrom()
|
||||||
|
if !ok {
|
||||||
|
return intToStringOmitZero(tgutil.ChatIdFromPeer(message.GetPeerID()))
|
||||||
|
}
|
||||||
|
fwdFrom, ok := fwdHeader.GetFromID()
|
||||||
|
if !ok {
|
||||||
|
return intToStringOmitZero(tgutil.ChatIdFromPeer(message.GetPeerID()))
|
||||||
|
}
|
||||||
|
return intToStringOmitZero(tgutil.ChatIdFromPeer(fwdFrom))
|
||||||
|
}(),
|
||||||
|
}.ToMap()
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func intToStringOmitZero(i int64) string {
|
||||||
|
if i == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", i)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package msgelem
|
|
||||||
|
|
||||||
const (
|
|
||||||
SaveHelpText = `
|
|
||||||
使用方法:
|
|
||||||
|
|
||||||
1. 使用该命令回复要保存的文件, 可选文件名参数.
|
|
||||||
示例:
|
|
||||||
/save custom_file_name.mp4
|
|
||||||
|
|
||||||
2. 设置默认存储后, 发送 /save <频道ID/用户名> <消息ID范围> 来批量保存文件. 遵从存储规则, 若未匹配到任何规则则使用默认存储.
|
|
||||||
示例:
|
|
||||||
/save @acherkrau 114-514
|
|
||||||
`
|
|
||||||
)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package msgelem
|
|
||||||
|
|
||||||
const (
|
|
||||||
WatchHelpText = `
|
|
||||||
使用 /watch 命令监听一个聊天的消息, 并自动保存到默认存储中, 遵从存储规则.
|
|
||||||
|
|
||||||
命令语法:
|
|
||||||
/watch <chat_id> [filter]
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- <chat_id>: 聊天的 ID 或用户名
|
|
||||||
- [filter]: 可选, 格式为 过滤器类型:表达式 , 所有支持类型的过滤器请查看文档
|
|
||||||
|
|
||||||
命令示例:
|
|
||||||
/watch 2229835658 msgre:.*plana.*
|
|
||||||
|
|
||||||
这将监听 ID 为 2229835658 的聊天, 并转存所有包含 "plana" 的媒体消息
|
|
||||||
`
|
|
||||||
)
|
|
||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/common/utils/tphutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tphutil"
|
||||||
"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/pkg/enums/fnamest"
|
|
||||||
"github.com/krau/SaveAny-Bot/pkg/telegraph"
|
"github.com/krau/SaveAny-Bot/pkg/telegraph"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
)
|
)
|
||||||
@@ -42,15 +41,15 @@ func GetFileFromMessageWithReply(ctx *ext.Context, update *ext.Update, message *
|
|||||||
logger.Errorf("Failed to reply: %s", err)
|
logger.Errorf("Failed to reply: %s", err)
|
||||||
return nil, nil, dispatcher.EndGroups
|
return nil, nil, dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
options := []tfile.TGFileOption{
|
// options := []tfile.TGFileOption{
|
||||||
tfile.WithMessage(message),
|
// tfile.WithMessage(message),
|
||||||
}
|
// }
|
||||||
if len(tfileopts) > 0 {
|
// if len(tfileopts) > 0 {
|
||||||
options = append(options, tfileopts...)
|
// options = append(options, tfileopts...)
|
||||||
} else {
|
// } else {
|
||||||
options = append(options, tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*message)))
|
// options = append(options, tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*message)))
|
||||||
}
|
// }
|
||||||
file, err = tfile.FromMediaMessage(media, ctx.Raw, message, options...)
|
file, err = tfile.FromMediaMessage(media, ctx.Raw, message, tfileopts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to get file from media: %s", err)
|
logger.Errorf("Failed to get file from media: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取文件失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString("获取文件失败: "+err.Error()), nil)
|
||||||
@@ -64,7 +63,7 @@ type EditMessageFunc func(text string, markup tg.ReplyMarkupClass)
|
|||||||
// 获取链接中的文件并回复等待消息
|
// 获取链接中的文件并回复等待消息
|
||||||
func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Update) (replied *types.Message, files []tfile.TGFileMessage, editReplied EditMessageFunc, err error) {
|
func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Update) (replied *types.Message, files []tfile.TGFileMessage, editReplied EditMessageFunc, err error) {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
msgLinks := re.TgMessageLinkRegexp.FindAllString(update.EffectiveMessage.GetMessage(), -1)
|
msgLinks := re.TgMessageLinkRegexp.FindAllString(tgutil.ExtractMessageEntityUrlsText(update.EffectiveMessage.Message), -1)
|
||||||
if len(msgLinks) == 0 {
|
if len(msgLinks) == 0 {
|
||||||
logger.Warn("no matched message links but called handleMessageLink")
|
logger.Warn("no matched message links but called handleMessageLink")
|
||||||
return nil, nil, nil, dispatcher.EndGroups
|
return nil, nil, nil, dispatcher.EndGroups
|
||||||
@@ -100,14 +99,15 @@ func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Up
|
|||||||
logger.Debugf("message %d has no media", msg.GetID())
|
logger.Debugf("message %d has no media", msg.GetID())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var opt tfile.TGFileOption
|
// var opt tfile.TGFileOption
|
||||||
switch user.FilenameStrategy {
|
// switch user.FilenameStrategy {
|
||||||
case fnamest.Message.String():
|
// case fnamest.Message.String():
|
||||||
opt = tfile.WithName(tgutil.GenFileNameFromMessage(*msg))
|
// opt = tfile.WithName(tgutil.GenFileNameFromMessage(*msg))
|
||||||
default:
|
// default:
|
||||||
opt = tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*msg))
|
// opt = tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*msg))
|
||||||
}
|
// }
|
||||||
file, err := tfile.FromMediaMessage(media, client, msg, opt)
|
opts := mediautil.TfileOptions(ctx, user, msg)
|
||||||
|
file, err := tfile.FromMediaMessage(media, client, msg, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("failed to create file from media: %s", err)
|
logger.Errorf("failed to create file from media: %s", err)
|
||||||
return
|
return
|
||||||
@@ -178,7 +178,7 @@ type TelegraphResult struct {
|
|||||||
// return replied message, image urls, telegraph path(unescaped), error
|
// return replied message, image urls, telegraph path(unescaped), error
|
||||||
func GetTphPicsFromMessageWithReply(ctx *ext.Context, update *ext.Update) (*types.Message, *TelegraphResult, error) {
|
func GetTphPicsFromMessageWithReply(ctx *ext.Context, update *ext.Update) (*types.Message, *TelegraphResult, error) {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
tphurl := re.TelegraphUrlRegexp.FindString(update.EffectiveMessage.GetMessage()) // TODO: batch urls
|
tphurl := re.TelegraphUrlRegexp.FindString(tgutil.ExtractMessageEntityUrlsText(update.EffectiveMessage.Message))
|
||||||
if tphurl == "" {
|
if tphurl == "" {
|
||||||
logger.Warnf("No telegraph url found but called handleTelegraph")
|
logger.Warnf("No telegraph url found but called handleTelegraph")
|
||||||
return nil, nil, dispatcher.ContinueGroups
|
return nil, nil, dispatcher.ContinueGroups
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"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/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/ruleutil"
|
||||||
|
userclient "github.com/krau/SaveAny-Bot/client/user"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
|
"github.com/krau/SaveAny-Bot/core/tasks/tfile"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/fnamest"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/rs/xid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleWatchCmd(ctx *ext.Context, update *ext.Update) error {
|
func handleWatchCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
args := strings.Split(update.EffectiveMessage.Text, " ")
|
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
ctx.Reply(update, ext.ReplyTextString(msgelem.WatchHelpText), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchHelpText)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
userChatID := update.GetUserChat().GetID()
|
userChatID := update.GetUserChat().GetID()
|
||||||
@@ -108,3 +119,106 @@ func handleUnwatchCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ctx.Reply(update, ext.ReplyTextString("已取消监听聊天: "+chatArg), nil)
|
ctx.Reply(update, ext.ReplyTextString("已取消监听聊天: "+chatArg), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
|
||||||
|
logger := log.FromContext(userclient.GetCtx())
|
||||||
|
for event := range ch {
|
||||||
|
logger.Debug("Received media message event", "chat_id", event.ChatID, "file_name", event.File.Name())
|
||||||
|
ctx := event.Ctx
|
||||||
|
file := event.File
|
||||||
|
chats, err := database.GetWatchChatsByChatID(ctx, event.ChatID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to get watch chats for chat ID %d: %v", event.ChatID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msgText := event.File.Message().GetMessage()
|
||||||
|
for _, chat := range chats {
|
||||||
|
if chat.Filter != "" {
|
||||||
|
filter := strings.Split(chat.Filter, ":")
|
||||||
|
if len(filter) != 2 {
|
||||||
|
logger.Warnf("Invalid filter format in chat %d, skipping", chat.ChatID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filterType := filter[0]
|
||||||
|
filterData := filter[1]
|
||||||
|
switch filterType {
|
||||||
|
case "msgre": // [TODO] enums for filter types
|
||||||
|
if ok, err := regexp.MatchString(filterData, msgText); err != nil {
|
||||||
|
continue
|
||||||
|
} else if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logger.Warnf("Unsupported filter type %s in chat %d, skipping", filterType, chat.ChatID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user, err := database.GetUserByID(ctx, chat.UserID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to get user by ID %d: %v", chat.UserID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if user.DefaultStorage == "" {
|
||||||
|
logger.Warnf("User %d has no default storage set, skipping media message handling", chat.UserID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stor, err := storage.GetStorageByUserIDAndName(ctx, user.ChatID, user.DefaultStorage)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to get storage by user ID %d and name %s: %v", user.ChatID, user.DefaultStorage, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch user.FilenameStrategy {
|
||||||
|
case fnamest.Message.String():
|
||||||
|
file.SetName(tgutil.GenFileNameFromMessage(*file.Message()))
|
||||||
|
case fnamest.Template.String():
|
||||||
|
if user.FilenameTemplate == "" {
|
||||||
|
logger.Warnf("Empty filename template for user %d, using default filename", user.ChatID)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
message := file.Message()
|
||||||
|
tmpl, err := template.New("filename").Parse(user.FilenameTemplate)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to parse filename template for user %d: %s", user.ChatID, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
data := mediautil.BuildFilenameTemplateData(message)
|
||||||
|
var sb strings.Builder
|
||||||
|
err = tmpl.Execute(&sb, data)
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Errorf("failed to execute filename template: %s", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
file.SetName(sb.String())
|
||||||
|
}
|
||||||
|
var dirPath string
|
||||||
|
if user.ApplyRule && user.Rules != nil {
|
||||||
|
matched, matchedStorageName, matchedDirPath := ruleutil.ApplyRule(ctx, user.Rules, ruleutil.NewInput(file))
|
||||||
|
if !matched {
|
||||||
|
goto startCreateTask
|
||||||
|
}
|
||||||
|
dirPath = matchedDirPath.String()
|
||||||
|
if matchedStorageName.IsUsable() {
|
||||||
|
stor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, matchedStorageName.String())
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to get storage by user ID and name: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startCreateTask:
|
||||||
|
storagePath := stor.JoinStoragePath(path.Join(dirPath, file.Name()))
|
||||||
|
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
|
||||||
|
taskid := xid.New().String()
|
||||||
|
task, err := tfile.NewTGFileTask(taskid, injectCtx, file, stor, storagePath, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("create task failed: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := core.AddTask(injectCtx, task); err != nil {
|
||||||
|
logger.Errorf("add task failed: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Infof("Added media message task for user %d in chat %d: %s", chat.UserID, event.ChatID, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -20,28 +21,27 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
keys := make(map[string]struct{})
|
keys := make(map[string]struct{})
|
||||||
re := regexp.MustCompile(`^\s*\[+\s*([^\]\[]+)\s*\]+`)
|
|
||||||
|
|
||||||
err := filepath.WalkDir(*dir, func(path string, d fs.DirEntry, err error) error {
|
err := filepath.WalkDir(*dir, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if d.IsDir() || !strings.HasSuffix(d.Name(), ".toml") {
|
if d.IsDir() || !(strings.HasSuffix(d.Name(), ".yaml") || strings.HasSuffix(d.Name(), ".yml")) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
f, err := os.Open(path)
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
s := bufio.NewScanner(f)
|
var content map[string]interface{}
|
||||||
for s.Scan() {
|
if err := yaml.Unmarshal(data, &content); err != nil {
|
||||||
if m := re.FindStringSubmatch(s.Text()); m != nil {
|
return fmt.Errorf("failed to parse yaml %s: %w", path, err)
|
||||||
keys[m[1]] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return s.Err()
|
|
||||||
|
collectKeys(content, "", keys)
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error walking directory: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error walking directory: %v\n", err)
|
||||||
@@ -62,23 +62,44 @@ func main() {
|
|||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
w := bufio.NewWriter(f)
|
w := bufio.NewWriter(f)
|
||||||
fmt.Fprintf(w, "// Code generated by cmd/gen_i18n. DO NOT EDIT.\n")
|
fmt.Fprintf(w, "// Code generated by cmd/geni18n. DO NOT EDIT.\n")
|
||||||
fmt.Fprintf(w, "package %s\n\n", *pkg)
|
fmt.Fprintf(w, "package %s\n\n", *pkg)
|
||||||
|
fmt.Fprintf(w, "type Key string\n\n")
|
||||||
fmt.Fprintf(w, "const (\n")
|
fmt.Fprintf(w, "const (\n")
|
||||||
for _, key := range list {
|
for _, key := range list {
|
||||||
name := toPascal(key)
|
name := toPascal(key)
|
||||||
fmt.Fprintf(w, "\t%s = %q\n", name, key)
|
fmt.Fprintf(w, "\t%s Key = %q\n", name, key)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, ")\n")
|
fmt.Fprintf(w, ")\n")
|
||||||
w.Flush()
|
w.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectKeys(node map[string]interface{}, prefix string, keys map[string]struct{}) {
|
||||||
|
for k, v := range node {
|
||||||
|
fullKey := k
|
||||||
|
if prefix != "" {
|
||||||
|
fullKey = prefix + "." + k
|
||||||
|
}
|
||||||
|
switch val := v.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
collectKeys(val, fullKey, keys)
|
||||||
|
default:
|
||||||
|
keys[fullKey] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转 PascalCase
|
||||||
func toPascal(key string) string {
|
func toPascal(key string) string {
|
||||||
parts := strings.Split(key, ".")
|
parts := strings.Split(key, ".")
|
||||||
for i, p := range parts {
|
for i, p := range parts {
|
||||||
if len(p) > 0 {
|
subs := strings.Split(p, "_")
|
||||||
parts[i] = strings.ToUpper(string(p[0])) + p[1:]
|
for j, s := range subs {
|
||||||
|
if len(s) > 0 {
|
||||||
|
subs[j] = strings.ToUpper(s[:1]) + s[1:]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
parts[i] = strings.Join(subs, "")
|
||||||
}
|
}
|
||||||
return strings.Join(parts, "")
|
return strings.Join(parts, "")
|
||||||
}
|
}
|
||||||
|
|||||||
29
cmd/run.go
29
cmd/run.go
@@ -36,7 +36,7 @@ func Run(cmd *cobra.Command, _ []string) {
|
|||||||
|
|
||||||
exitChan, err := initAll(ctx)
|
exitChan, err := initAll(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Failed to initialize", "error", err)
|
logger.Fatal("Init failed", "error", err)
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
<-exitChan
|
<-exitChan
|
||||||
@@ -46,35 +46,36 @@ func Run(cmd *cobra.Command, _ []string) {
|
|||||||
core.Run(ctx)
|
core.Run(ctx)
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
logger.Info(i18n.T(i18nk.Exiting))
|
logger.Info(i18n.T(i18nk.LifetimeExiting))
|
||||||
defer logger.Info(i18n.T(i18nk.Bye))
|
defer logger.Info(i18n.T(i18nk.LifetimeBye))
|
||||||
cleanCache()
|
cleanCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initAll(ctx context.Context) (<-chan struct{}, error) {
|
func initAll(ctx context.Context) (<-chan struct{}, error) {
|
||||||
if err := config.Init(ctx); err != nil {
|
if err := config.Init(ctx); err != nil {
|
||||||
fmt.Println("Failed to load config:", err)
|
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
cache.Init()
|
cache.Init()
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
i18n.Init(config.C().Lang)
|
i18n.Init(config.C().Lang)
|
||||||
logger.Info(i18n.T(i18nk.Initing))
|
logger.Info(i18n.T(i18nk.LifetimeIniting))
|
||||||
database.Init(ctx)
|
database.Init(ctx)
|
||||||
storage.LoadStorages(ctx)
|
storage.LoadStorages(ctx)
|
||||||
if config.C().Parser.PluginEnable {
|
if config.C().Parser.PluginEnable {
|
||||||
for _, dir := range config.C().Parser.PluginDirs {
|
for _, dir := range config.C().Parser.PluginDirs {
|
||||||
if err := parsers.LoadPlugins(ctx, dir); err != nil {
|
if err := parsers.LoadPlugins(ctx, dir); err != nil {
|
||||||
logger.Error("Failed to load parser plugins", "dir", dir, "error", err)
|
logger.Error(i18n.T(i18nk.ParserPluginLoadFailed), "dir", dir, "error", err)
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("Loaded parser plugins", "dir", dir)
|
logger.Debug(i18n.T(i18nk.ParserPluginLoadedDir), "dir", dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config.C().Telegram.Userbot.Enable {
|
if config.C().Telegram.Userbot.Enable {
|
||||||
_, err := userclient.Login(ctx)
|
_, err := userclient.Login(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("User client login failed: %s", err)
|
logger.Fatal(i18n.T(i18nk.LifetimeUserLoginFailed, map[string]any{
|
||||||
|
"Error": err,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bot.Init(ctx), nil
|
return bot.Init(ctx), nil
|
||||||
@@ -86,14 +87,14 @@ func cleanCache() {
|
|||||||
}
|
}
|
||||||
if config.C().Temp.BasePath != "" && !config.C().Stream {
|
if config.C().Temp.BasePath != "" && !config.C().Stream {
|
||||||
if slices.Contains([]string{"/", ".", "\\", ".."}, filepath.Clean(config.C().Temp.BasePath)) {
|
if slices.Contains([]string{"/", ".", "\\", ".."}, filepath.Clean(config.C().Temp.BasePath)) {
|
||||||
log.Error(i18n.T(i18nk.InvalidCacheDir, map[string]any{
|
log.Error(i18n.T(i18nk.ConfigErrInvalidCacheDir, map[string]any{
|
||||||
"Path": config.C().Temp.BasePath,
|
"Path": config.C().Temp.BasePath,
|
||||||
}))
|
}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentDir, err := os.Getwd()
|
currentDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(i18n.T(i18nk.GetWorkdirFailed, map[string]any{
|
log.Error(i18n.T(i18nk.ErrGetWorkdirFailed, map[string]any{
|
||||||
"Error": err,
|
"Error": err,
|
||||||
}))
|
}))
|
||||||
return
|
return
|
||||||
@@ -101,16 +102,16 @@ func cleanCache() {
|
|||||||
cachePath := filepath.Join(currentDir, config.C().Temp.BasePath)
|
cachePath := filepath.Join(currentDir, config.C().Temp.BasePath)
|
||||||
cachePath, err = filepath.Abs(cachePath)
|
cachePath, err = filepath.Abs(cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(i18n.T(i18nk.GetCacheAbsPathFailed, map[string]any{
|
log.Error(i18n.T(i18nk.ErrGetCacheAbsPathFailed, map[string]any{
|
||||||
"Error": err,
|
"Error": err,
|
||||||
}))
|
}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Info(i18n.T(i18nk.CleaningCache, map[string]any{
|
log.Info(i18n.T(i18nk.LifetimeCleaningCache, map[string]any{
|
||||||
"Path": cachePath,
|
"Path": cachePath,
|
||||||
}))
|
}))
|
||||||
if err := fsutil.RemoveAllInDir(cachePath); err != nil {
|
if err := fsutil.RemoveAllInDir(cachePath); err != nil {
|
||||||
log.Error(i18n.T(i18nk.CleanCacheFailed, map[string]any{
|
log.Error(i18n.T(i18nk.ErrCleanCacheFailed, map[string]any{
|
||||||
"Error": err,
|
"Error": err,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ var upgradeCmd = &cobra.Command{
|
|||||||
v := semver.MustParse(config.Version)
|
v := semver.MustParse(config.Version)
|
||||||
latest, err := selfupdate.UpdateSelf(v, config.GitRepo)
|
latest, err := selfupdate.UpdateSelf(v, config.GitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Binary update failed:", err)
|
fmt.Println("Update failed:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if latest.Version.Equals(v) {
|
if latest.Version.Equals(v) {
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import (
|
|||||||
|
|
||||||
"maps"
|
"maps"
|
||||||
|
|
||||||
|
"github.com/goccy/go-yaml"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
"github.com/pelletier/go-toml/v2"
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed locale/*.toml
|
//go:embed locale/*
|
||||||
var localesFS embed.FS
|
var localesFS embed.FS
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -20,7 +21,7 @@ var (
|
|||||||
|
|
||||||
func Init(lang string) {
|
func Init(lang string) {
|
||||||
bundle = i18n.NewBundle(language.SimplifiedChinese)
|
bundle = i18n.NewBundle(language.SimplifiedChinese)
|
||||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
|
||||||
files, err := localesFS.ReadDir("locale")
|
files, err := localesFS.ReadDir("locale")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("failed to read locale directory: " + err.Error())
|
panic("failed to read locale directory: " + err.Error())
|
||||||
@@ -39,7 +40,7 @@ func Init(lang string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func T(key string, templateData ...map[string]any) string {
|
func T(key i18nk.Key, templateData ...map[string]any) string {
|
||||||
if localizer == nil || bundle == nil {
|
if localizer == nil || bundle == nil {
|
||||||
panic("localizer or bundle is not initialized, call Init() first")
|
panic("localizer or bundle is not initialized, call Init() first")
|
||||||
}
|
}
|
||||||
@@ -48,11 +49,11 @@ func T(key string, templateData ...map[string]any) string {
|
|||||||
maps.Copy(templateDataMap, data)
|
maps.Copy(templateDataMap, data)
|
||||||
}
|
}
|
||||||
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
||||||
MessageID: key,
|
MessageID: string(key),
|
||||||
TemplateData: templateDataMap,
|
TemplateData: templateDataMap,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return key
|
return string(key)
|
||||||
}
|
}
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
@@ -77,32 +78,32 @@ func TWithLang(lang, key string, templateData ...map[string]any) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only use in tests or packages that load before i18n
|
// Only use in tests or packages that load before i18n
|
||||||
func TWithoutInit(lang, key string, templateData ...map[string]any) string {
|
func TWithoutInit(lang string, key i18nk.Key, templateData ...map[string]any) string {
|
||||||
bundle := i18n.NewBundle(language.SimplifiedChinese)
|
bundle := i18n.NewBundle(language.SimplifiedChinese)
|
||||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
|
||||||
files, err := localesFS.ReadDir("locale")
|
files, err := localesFS.ReadDir("locale")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return key
|
return string(key)
|
||||||
}
|
}
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if _, err := bundle.LoadMessageFileFS(localesFS, "locale/"+file.Name()); err != nil {
|
if _, err := bundle.LoadMessageFileFS(localesFS, "locale/"+file.Name()); err != nil {
|
||||||
return key
|
return string(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
localizer := i18n.NewLocalizer(bundle, lang)
|
localizer := i18n.NewLocalizer(bundle, lang)
|
||||||
if localizer == nil {
|
if localizer == nil {
|
||||||
return key
|
return string(key)
|
||||||
}
|
}
|
||||||
templateDataMap := make(map[string]any)
|
templateDataMap := make(map[string]any)
|
||||||
for _, data := range templateData {
|
for _, data := range templateData {
|
||||||
maps.Copy(templateDataMap, data)
|
maps.Copy(templateDataMap, data)
|
||||||
}
|
}
|
||||||
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
||||||
MessageID: key,
|
MessageID: string(key),
|
||||||
TemplateData: templateDataMap,
|
TemplateData: templateDataMap,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return key
|
return string(key)
|
||||||
}
|
}
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
// Code generated by cmd/gen_i18n. DO NOT EDIT.
|
// Code generated by cmd/geni18n. DO NOT EDIT.
|
||||||
package i18nk
|
package i18nk
|
||||||
|
|
||||||
|
type Key string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CleanCacheFailed = "CleanCacheFailed"
|
BotMsgHelpTextFmt Key = "bot.msg.help_text_fmt"
|
||||||
CleaningCache = "CleaningCache"
|
BotMsgSaveHelpText Key = "bot.msg.save_help_text"
|
||||||
ConfigInvalidDuplicateStorageName = "ConfigInvalid.DuplicateStorageName"
|
BotMsgWatchHelpText Key = "bot.msg.watch_help_text"
|
||||||
ConfigInvalidWorkersOrRetry = "ConfigInvalid.WorkersOrRetry"
|
ConfigErrDuplicateStorageName Key = "config.err.duplicate_storage_name"
|
||||||
CreateRmTimerFailed = "CreateRmTimerFailed"
|
ConfigErrInvalidCacheDir Key = "config.err.invalid_cache_dir"
|
||||||
GetCacheAbsPathFailed = "GetCacheAbsPathFailed"
|
ConfigLoadedStorages Key = "config.loaded_storages"
|
||||||
GetWorkdirFailed = "GetWorkdirFailed"
|
ErrCleanCacheFailed Key = "err.clean_cache_failed"
|
||||||
InvalidCacheDir = "InvalidCacheDir"
|
ErrGetCacheAbsPathFailed Key = "err.get_cache_abs_path_failed"
|
||||||
LoadedStorages = "LoadedStorages"
|
ErrGetWorkdirFailed Key = "err.get_workdir_failed"
|
||||||
RemoveFileAfter = "RemoveFileAfter"
|
LifetimeBye Key = "lifetime.bye"
|
||||||
RemoveFileFailed = "RemoveFileFailed"
|
LifetimeCleaningCache Key = "lifetime.cleaning_cache"
|
||||||
Bye = "bye"
|
LifetimeExiting Key = "lifetime.exiting"
|
||||||
Exiting = "exiting"
|
LifetimeInitfailed Key = "lifetime.initfailed"
|
||||||
Initing = "initing"
|
LifetimeIniting Key = "lifetime.initing"
|
||||||
|
LifetimeUserLoginFailed Key = "lifetime.user_login_failed"
|
||||||
|
ParserPluginLoadFailed Key = "parser.plugin.load_failed"
|
||||||
|
ParserPluginLoadedDir Key = "parser.plugin.loaded_dir"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
[initing]
|
|
||||||
other = "正在启动..."
|
|
||||||
[exiting]
|
|
||||||
other = "正在退出..."
|
|
||||||
[bye]
|
|
||||||
other = "已退出"
|
|
||||||
[InvalidCacheDir]
|
|
||||||
other = "无效的缓存文件夹: {{.Path}}"
|
|
||||||
[GetWorkdirFailed]
|
|
||||||
other = "获取工作目录失败: {{.Error}}"
|
|
||||||
[GetCacheAbsPathFailed]
|
|
||||||
other = "获取缓存绝对路径失败: {{.Error}}"
|
|
||||||
[CleaningCache]
|
|
||||||
other = "正在清理缓存文件夹: {{.Path}}"
|
|
||||||
[CleanCacheFailed]
|
|
||||||
other = "清理缓存失败: {{.Error}}"
|
|
||||||
[CreateRmTimerFailed]
|
|
||||||
other = "创建清理定时器失败, 路径: {{.Path}}, 错误: {{.Error}}"
|
|
||||||
[RemoveFileAfter]
|
|
||||||
other = "将在 {{.Duration}} 后删除文件: {{.Path}}"
|
|
||||||
[RemoveFileFailed]
|
|
||||||
other = "删除文件失败: {{.Path}}, 错误: {{.Error}}"
|
|
||||||
[LoadedStorages]
|
|
||||||
other = "已加载 {{.Count}} 个存储"
|
|
||||||
[ConfigInvalid.WorkersOrRetry]
|
|
||||||
other = "配置无效: workers 或 retry 必须大于 0, 但当前值为: workers={{.Workers}}, retry={{.Retry}}"
|
|
||||||
[ConfigInvalid.DuplicateStorageName]
|
|
||||||
other = "存储名称重复: {{.Name}}"
|
|
||||||
62
common/i18n/locale/zh-Hans.yaml
Normal file
62
common/i18n/locale/zh-Hans.yaml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
lifetime:
|
||||||
|
initing: 正在启动
|
||||||
|
initfailed: 初始化失败
|
||||||
|
exiting: 正在退出
|
||||||
|
user_login_failed: "用户登录失败: {{.Error}}"
|
||||||
|
cleaning_cache: "正在清理缓存 {{.Path}}"
|
||||||
|
bye: 已退出
|
||||||
|
config:
|
||||||
|
loaded_storages: "已加载 {{.Count}} 个存储后端"
|
||||||
|
err:
|
||||||
|
invalid_cache_dir: "无效的缓存目录: {{.Path}},请检查配置文件"
|
||||||
|
duplicate_storage_name: "存储名称 '{{.Name}}' 重复,请检查配置文件"
|
||||||
|
err:
|
||||||
|
get_workdir_failed: "获取工作目录失败: {{.Error}}"
|
||||||
|
get_cache_abs_path_failed: "获取缓存绝对路径失败: {{.Error}}"
|
||||||
|
clean_cache_failed: "清理缓存失败: {{.Error}}"
|
||||||
|
parser:
|
||||||
|
plugin:
|
||||||
|
load_failed: 加载解析器插件失败
|
||||||
|
loaded_dir: 解析器插件已加载
|
||||||
|
bot:
|
||||||
|
msg:
|
||||||
|
help_text_fmt: |
|
||||||
|
Save Any Bot - 转存你的 Telegram 文件
|
||||||
|
版本: %s , 提交: %s
|
||||||
|
|
||||||
|
命令:
|
||||||
|
/start - 开始使用
|
||||||
|
/help - 显示帮助
|
||||||
|
/silent - 开关静默模式
|
||||||
|
/storage - 设置默认存储位置
|
||||||
|
/save [自定义文件名] - 保存文件
|
||||||
|
/dir - 管理存储目录
|
||||||
|
/rule - 管理规则
|
||||||
|
/update - 检查更新并升级
|
||||||
|
|
||||||
|
使用帮助: https://sabot.unv.app/usage
|
||||||
|
反馈群组: https://t.me/ProjectSaveAny
|
||||||
|
save_help_text: |
|
||||||
|
使用方法:
|
||||||
|
|
||||||
|
1. 使用该命令回复要保存的文件, 可选文件名参数.
|
||||||
|
示例:
|
||||||
|
/save custom_file_name.mp4
|
||||||
|
|
||||||
|
2. 设置默认存储后, 发送 /save <频道ID/用户名> <消息ID范围> 来批量保存文件. 遵从存储规则, 若未匹配到任何规则则使用默认存储.
|
||||||
|
示例:
|
||||||
|
/save @acherkrau 114-514
|
||||||
|
watch_help_text: |
|
||||||
|
使用 /watch 命令监听一个聊天的消息, 并自动保存到默认存储中, 遵从存储规则.
|
||||||
|
|
||||||
|
命令语法:
|
||||||
|
/watch <chat_id> [filter]
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- <chat_id>: 聊天的 ID 或用户名
|
||||||
|
- [filter]: 可选, 格式为 过滤器类型:表达式 , 所有支持类型的过滤器请查看文档
|
||||||
|
|
||||||
|
命令示例:
|
||||||
|
/watch 2229835658 msgre:.*plana.*
|
||||||
|
|
||||||
|
这将监听 ID 为 2229835658 的聊天, 并转存所有包含 "plana" 的媒体消息
|
||||||
@@ -43,11 +43,10 @@ func NewProxyHTTPClient(proxyUrl string) (*http.Client, error) {
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
case "socks5":
|
case "socks5":
|
||||||
dialer, err := proxy.SOCKS5("tcp", u.Host, nil, proxy.Direct)
|
dialer, err := proxy.FromURL(u, proxy.Direct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unicode/utf16"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/duke-git/lancet/v2/maputil"
|
"github.com/duke-git/lancet/v2/maputil"
|
||||||
@@ -306,3 +307,49 @@ func GetGroupedMessages(ctx *ext.Context, chatID int64, msg *tg.Message) ([]*tg.
|
|||||||
}
|
}
|
||||||
return groupedMessages, nil
|
return groupedMessages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExtractMessageEntityUrls(msg *tg.Message) []string {
|
||||||
|
if len(msg.Entities) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
msgText := msg.GetMessage()
|
||||||
|
if msgText == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
runes := []rune(msgText)
|
||||||
|
utf16Codes := utf16.Encode(runes)
|
||||||
|
|
||||||
|
var urls []string
|
||||||
|
for _, entity := range msg.Entities {
|
||||||
|
switch ent := entity.(type) {
|
||||||
|
case *tg.MessageEntityTextURL:
|
||||||
|
urls = append(urls, ent.GetURL())
|
||||||
|
case *tg.MessageEntityURL:
|
||||||
|
start := ent.Offset
|
||||||
|
end := ent.Offset + ent.Length
|
||||||
|
if start < 0 || end > len(utf16Codes) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subRunes := utf16.Decode(utf16Codes[start:end])
|
||||||
|
urls = append(urls, string(subRunes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractMessageEntityUrlsText(msg *tg.Message) string {
|
||||||
|
if msg == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
urls := ExtractMessageEntityUrls(msg)
|
||||||
|
if len(urls) == 0 {
|
||||||
|
return msg.GetMessage()
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, url := range urls {
|
||||||
|
sb.WriteString(url)
|
||||||
|
sb.WriteString(" ")
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|||||||
16
common/utils/tgutil/peer.go
Normal file
16
common/utils/tgutil/peer.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package tgutil
|
||||||
|
|
||||||
|
import "github.com/gotd/td/tg"
|
||||||
|
|
||||||
|
func ChatIdFromPeer(peer tg.PeerClass) int64 {
|
||||||
|
switch peer := peer.(type) {
|
||||||
|
case *tg.PeerChannel:
|
||||||
|
return peer.ChannelID
|
||||||
|
case *tg.PeerUser:
|
||||||
|
return peer.UserID
|
||||||
|
case *tg.PeerChat:
|
||||||
|
return peer.ChatID
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,25 +112,28 @@ func Init(ctx context.Context) error {
|
|||||||
storageNames := make(map[string]struct{})
|
storageNames := make(map[string]struct{})
|
||||||
for _, storage := range cfg.Storages {
|
for _, storage := range cfg.Storages {
|
||||||
if _, ok := storageNames[storage.GetName()]; ok {
|
if _, ok := storageNames[storage.GetName()]; ok {
|
||||||
return errors.New(i18n.TWithoutInit(cfg.Lang, i18nk.ConfigInvalidDuplicateStorageName, map[string]any{
|
return errors.New(i18n.TWithoutInit(cfg.Lang, i18nk.ConfigErrDuplicateStorageName, map[string]any{
|
||||||
"Name": storage.GetName(),
|
"Name": storage.GetName(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
storageNames[storage.GetName()] = struct{}{}
|
storageNames[storage.GetName()] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(i18n.TWithoutInit(cfg.Lang, i18nk.LoadedStorages, map[string]any{
|
fmt.Println(i18n.TWithoutInit(cfg.Lang, i18nk.ConfigLoadedStorages, map[string]any{
|
||||||
"Count": len(cfg.Storages),
|
"Count": len(cfg.Storages),
|
||||||
}))
|
}))
|
||||||
for _, storage := range cfg.Storages {
|
for _, storage := range cfg.Storages {
|
||||||
fmt.Printf(" - %s (%s)\n", storage.GetName(), storage.GetType())
|
fmt.Printf(" - %s (%s)\n", storage.GetName(), storage.GetType())
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Workers < 1 || cfg.Retry < 1 {
|
if cfg.Workers < 1 {
|
||||||
return errors.New(i18n.TWithoutInit(cfg.Lang, i18nk.ConfigInvalidWorkersOrRetry, map[string]any{
|
cfg.Workers = 1
|
||||||
"Workers": cfg.Workers,
|
}
|
||||||
"Retry": cfg.Retry,
|
if cfg.Threads < 1 {
|
||||||
}))
|
cfg.Threads = 1
|
||||||
|
}
|
||||||
|
if cfg.Retry < 1 {
|
||||||
|
cfg.Retry = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, storage := range cfg.Storages {
|
for _, storage := range cfg.Storages {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type User struct {
|
|||||||
Rules []Rule
|
Rules []Rule
|
||||||
WatchChats []WatchChat
|
WatchChats []WatchChat
|
||||||
FilenameStrategy string
|
FilenameStrategy string
|
||||||
|
FilenameTemplate string
|
||||||
}
|
}
|
||||||
|
|
||||||
type WatchChat struct {
|
type WatchChat struct {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ After=systemd-user-sessions.service
|
|||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/yourpath/
|
WorkingDirectory=/yourpath/
|
||||||
ExecStart=/yourpath/saveany-bot
|
ExecStart=/yourpath/saveany-bot
|
||||||
Restart=on-failure
|
Restart=always
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Stream 模式对于磁盘空间有限的部署环境十分有用, 但也有一
|
|||||||
<br />
|
<br />
|
||||||
开启 userbot 集成后第一次启动 bot 时需要通过终端交互输入手机号, 2FA 和验证码.
|
开启 userbot 集成后第一次启动 bot 时需要通过终端交互输入手机号, 2FA 和验证码.
|
||||||
<br />
|
<br />
|
||||||
如果你使用 docker 部署, 请进入容器内执行相关操作.
|
如果你使用 docker 部署, 请使用 -it 参数为容器提供交互式环境, 然后执行登录操作.
|
||||||
{{< /hint >}}
|
{{< /hint >}}
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ After=systemd-user-sessions.service
|
|||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/yourpath/
|
WorkingDirectory=/yourpath/
|
||||||
ExecStart=/yourpath/saveany-bot
|
ExecStart=/yourpath/saveany-bot
|
||||||
Restart=on-failure
|
Restart=always
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -10,6 +10,7 @@ require (
|
|||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
github.com/fatih/color v1.18.0
|
github.com/fatih/color v1.18.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9
|
github.com/gabriel-vasile/mimetype v1.4.9
|
||||||
|
github.com/goccy/go-yaml v1.18.0
|
||||||
github.com/gotd/contrib v0.21.0
|
github.com/gotd/contrib v0.21.0
|
||||||
github.com/gotd/td v0.129.0
|
github.com/gotd/td v0.129.0
|
||||||
github.com/minio/minio-go/v7 v7.0.95
|
github.com/minio/minio-go/v7 v7.0.95
|
||||||
@@ -83,7 +84,7 @@ require (
|
|||||||
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.9.0 // indirect
|
||||||
github.com/tinylib/msgp v1.3.0 // indirect
|
github.com/tinylib/msgp v1.3.0 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
github.com/ulikunitz/xz v0.5.14 // indirect
|
||||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
@@ -114,7 +115,7 @@ require (
|
|||||||
github.com/ncruces/go-sqlite3 v0.27.1
|
github.com/ncruces/go-sqlite3 v0.27.1
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.24.0
|
github.com/ncruces/go-sqlite3/gormlite v0.24.0
|
||||||
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
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.10.0 // indirect
|
github.com/sagikazarmark/locafero v0.10.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.14.0 // indirect
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -109,6 +109,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
|
|||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
@@ -245,8 +247,8 @@ github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+C
|
|||||||
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||||
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||||
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=
|
||||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
||||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.14/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=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/cmd"
|
"github.com/krau/SaveAny-Bot/cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:generate go run cmd/geni18n/main.go -dir ./common/i18n/locale -out common/i18n/i18nk/keys.go -pkg i18nk
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
122
parsers/js.go
122
parsers/js.go
@@ -4,12 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"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/pkg/parser"
|
"github.com/krau/SaveAny-Bot/pkg/parser"
|
||||||
@@ -101,50 +98,6 @@ func newJSParser(vm *goja.Runtime, canHandleFunc, parseFunc goja.Value, metadata
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerParser(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
|
|
||||||
return func(call goja.FunctionCall) goja.Value {
|
|
||||||
jsObj := call.Argument(0)
|
|
||||||
if jsObj == nil || goja.IsUndefined(jsObj) || goja.IsNull(jsObj) {
|
|
||||||
panic("registerParser expects an object { canHandle, parse }")
|
|
||||||
}
|
|
||||||
|
|
||||||
obj := jsObj.ToObject(vm)
|
|
||||||
if obj == nil {
|
|
||||||
panic("registerParser: cannot convert argument to object")
|
|
||||||
}
|
|
||||||
metaValue := obj.Get("metadata")
|
|
||||||
if metaValue == nil || goja.IsUndefined(metaValue) {
|
|
||||||
panic("parser must provide metadata")
|
|
||||||
}
|
|
||||||
var metadata PluginMeta
|
|
||||||
if exported := metaValue.Export(); exported != nil {
|
|
||||||
data, err := json.Marshal(exported)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to marshal metadata to JSON: %v", err))
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to unmarshal JSON to PluginMeta: %v", err))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic("metadata cannot be null or undefined")
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginV := semver.MustParse(metadata.Version)
|
|
||||||
if pluginV.LT(MinimumParserVersion) || pluginV.GT(LatestParserVersion) {
|
|
||||||
panic(fmt.Sprintf("parser version %s is not supported, must be between %s and %s", metadata.Version, MinimumParserVersion, LatestParserVersion))
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFn := obj.Get("canHandle")
|
|
||||||
parseFn := obj.Get("parse")
|
|
||||||
if parseFn == nil || goja.IsUndefined(parseFn) {
|
|
||||||
panic("parser must provide a parse function")
|
|
||||||
}
|
|
||||||
|
|
||||||
parsers = append(parsers, newJSParser(vm, handleFn, parseFn, metadata))
|
|
||||||
return goja.Undefined()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadPlugins(ctx context.Context, dir string) error {
|
func LoadPlugins(ctx context.Context, dir string) error {
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -162,80 +115,13 @@ func LoadPlugins(ctx context.Context, dir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
vm.Set("registerParser", registerParser(vm))
|
vm.Set("registerParser", jsRegisterParser(vm))
|
||||||
// Inject some utils to vm
|
// Inject some utils to vm
|
||||||
logger := log.FromContext(ctx).WithPrefix(fmt.Sprintf("[plugin|parser]/%s", e.Name()))
|
logger := log.FromContext(ctx).WithPrefix(fmt.Sprintf("[plugin|parser]/%s", e.Name()))
|
||||||
vm.Set("console", map[string]any{
|
vm.Set("console", jsConsole(logger))
|
||||||
"log": func(args ...any) {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(args) > 1 {
|
|
||||||
logger.Info(args[0], args[1:]...)
|
|
||||||
} else {
|
|
||||||
logger.Info(args[0])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// http fetch funcs
|
// http fetch funcs
|
||||||
ghttp := vm.NewObject()
|
vm.Set("ghttp", jsGhttp(vm))
|
||||||
ghttp.Set("get", func(call goja.FunctionCall) goja.Value {
|
|
||||||
url := call.Argument(0).String()
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("failed to fetch %s: %v", url, err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("failed to fetch %s: %s", url, resp.Status),
|
|
||||||
"status": resp.StatusCode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Errorf("failed to read response body: %w", err).Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return vm.ToValue(string(body))
|
|
||||||
})
|
|
||||||
ghttp.Set("getJSON", func(call goja.FunctionCall) goja.Value {
|
|
||||||
url := call.Argument(0).String()
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("failed to fetch %s: %v", url, err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Sprintf("failed to fetch %s: %s", url, resp.Status),
|
|
||||||
"status": resp.StatusCode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Errorf("failed to read response body: %w", err).Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var jsonData map[string]any
|
|
||||||
if err := json.Unmarshal(body, &jsonData); err != nil {
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"error": fmt.Errorf("failed to unmarshal JSON: %w", err).Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return vm.ToValue(map[string]any{
|
|
||||||
"data": jsonData,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
vm.Set("ghttp", ghttp)
|
|
||||||
|
|
||||||
if _, err := vm.RunString(string(code)); err != nil {
|
if _, err := vm.RunString(string(code)); err != nil {
|
||||||
return fmt.Errorf("error loading plugin %s: %w", e.Name(), err)
|
return fmt.Errorf("error loading plugin %s: %w", e.Name(), err)
|
||||||
}
|
}
|
||||||
|
|||||||
151
parsers/js_api.go
Normal file
151
parsers/js_api.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/blang/semver"
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/netutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func jsRegisterParser(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
|
||||||
|
return func(call goja.FunctionCall) goja.Value {
|
||||||
|
jsObj := call.Argument(0)
|
||||||
|
if jsObj == nil || goja.IsUndefined(jsObj) || goja.IsNull(jsObj) {
|
||||||
|
panic("registerParser expects an object { canHandle, parse }")
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := jsObj.ToObject(vm)
|
||||||
|
if obj == nil {
|
||||||
|
panic("registerParser: cannot convert argument to object")
|
||||||
|
}
|
||||||
|
metaValue := obj.Get("metadata")
|
||||||
|
if metaValue == nil || goja.IsUndefined(metaValue) {
|
||||||
|
panic("parser must provide metadata")
|
||||||
|
}
|
||||||
|
var metadata PluginMeta
|
||||||
|
if exported := metaValue.Export(); exported != nil {
|
||||||
|
data, err := json.Marshal(exported)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to marshal metadata to JSON: %v", err))
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to unmarshal JSON to PluginMeta: %v", err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic("metadata cannot be null or undefined")
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginV := semver.MustParse(metadata.Version)
|
||||||
|
if pluginV.LT(MinimumParserVersion) || pluginV.GT(LatestParserVersion) {
|
||||||
|
panic(fmt.Sprintf("parser version %s is not supported, must be between %s and %s", metadata.Version, MinimumParserVersion, LatestParserVersion))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFn := obj.Get("canHandle")
|
||||||
|
parseFn := obj.Get("parse")
|
||||||
|
if parseFn == nil || goja.IsUndefined(parseFn) {
|
||||||
|
panic("parser must provide a parse function")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsers = append(parsers, newJSParser(vm, handleFn, parseFn, metadata))
|
||||||
|
return goja.Undefined()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsConsole = func(logger *log.Logger) map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"log": func(args ...any) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(args) > 1 {
|
||||||
|
logger.Info(args[0], args[1:]...)
|
||||||
|
} else {
|
||||||
|
logger.Info(args[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsGhttp = func(vm *goja.Runtime) *goja.Object {
|
||||||
|
ghttp := vm.NewObject()
|
||||||
|
client := netutil.DefaultParserHTTPClient()
|
||||||
|
ghttp.Set("get", func(call goja.FunctionCall) goja.Value {
|
||||||
|
url := call.Argument(0).String()
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to fetch %s: %v", url, err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to fetch %s: %s", url, resp.Status),
|
||||||
|
"status": resp.StatusCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Errorf("failed to read response body: %w", err).Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return vm.ToValue(string(body))
|
||||||
|
})
|
||||||
|
ghttp.Set("getJSON", func(call goja.FunctionCall) goja.Value {
|
||||||
|
url := call.Argument(0).String()
|
||||||
|
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to fetch %s: %v", url, err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to fetch %s: %s", url, resp.Status),
|
||||||
|
"status": resp.StatusCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Errorf("failed to read response body: %w", err).Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var jsonData map[string]any
|
||||||
|
if err := json.Unmarshal(body, &jsonData); err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Errorf("failed to unmarshal JSON: %w", err).Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"data": jsonData,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
ghttp.Set("head", func(call goja.FunctionCall) goja.Value {
|
||||||
|
url := call.Argument(0).String()
|
||||||
|
resp, err := client.Head(url)
|
||||||
|
if err != nil {
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"error": fmt.Sprintf("failed to fetch %s: %v", url, err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
headers := make(map[string]string)
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
headers[k] = v[0]
|
||||||
|
}
|
||||||
|
return vm.ToValue(map[string]any{
|
||||||
|
"status": resp.StatusCode,
|
||||||
|
"headers": headers,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return ghttp
|
||||||
|
}
|
||||||
@@ -4,11 +4,12 @@ package fnamest
|
|||||||
|
|
||||||
// FnameST
|
// FnameST
|
||||||
/* ENUM(
|
/* ENUM(
|
||||||
default, message
|
default, message, template
|
||||||
) */
|
) */
|
||||||
type FnameST string
|
type FnameST string
|
||||||
|
|
||||||
var FnameSTDisplay = map[FnameST]string{
|
var FnameSTDisplay = map[FnameST]string{
|
||||||
Default: "默认",
|
Default: "默认",
|
||||||
Message: "优先从消息生成",
|
Message: "优先从消息生成",
|
||||||
|
Template: "自定义模板",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const (
|
|||||||
Default FnameST = "default"
|
Default FnameST = "default"
|
||||||
// Message is a FnameST of type message.
|
// Message is a FnameST of type message.
|
||||||
Message FnameST = "message"
|
Message FnameST = "message"
|
||||||
|
// Template is a FnameST of type template.
|
||||||
|
Template FnameST = "template"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrInvalidFnameST = fmt.Errorf("not a valid FnameST, try [%s]", strings.Join(_FnameSTNames, ", "))
|
var ErrInvalidFnameST = fmt.Errorf("not a valid FnameST, try [%s]", strings.Join(_FnameSTNames, ", "))
|
||||||
@@ -23,6 +25,7 @@ var ErrInvalidFnameST = fmt.Errorf("not a valid FnameST, try [%s]", strings.Join
|
|||||||
var _FnameSTNames = []string{
|
var _FnameSTNames = []string{
|
||||||
string(Default),
|
string(Default),
|
||||||
string(Message),
|
string(Message),
|
||||||
|
string(Template),
|
||||||
}
|
}
|
||||||
|
|
||||||
// FnameSTNames returns a list of possible string values of FnameST.
|
// FnameSTNames returns a list of possible string values of FnameST.
|
||||||
@@ -37,6 +40,7 @@ func FnameSTValues() []FnameST {
|
|||||||
return []FnameST{
|
return []FnameST{
|
||||||
Default,
|
Default,
|
||||||
Message,
|
Message,
|
||||||
|
Template,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +57,9 @@ func (x FnameST) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _FnameSTValue = map[string]FnameST{
|
var _FnameSTValue = map[string]FnameST{
|
||||||
"default": Default,
|
"default": Default,
|
||||||
"message": Message,
|
"message": Message,
|
||||||
|
"template": Template,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFnameST attempts to convert a string to a FnameST.
|
// ParseFnameST attempts to convert a string to a FnameST.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type TGFile interface {
|
|||||||
Dler() downloader.Client // witch client to use for downloading
|
Dler() downloader.Client // witch client to use for downloading
|
||||||
Size() int64
|
Size() int64
|
||||||
Name() string
|
Name() string
|
||||||
|
SetName(name string)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TGFileMessage interface {
|
type TGFileMessage interface {
|
||||||
@@ -29,6 +30,10 @@ type tgFile struct {
|
|||||||
dler downloader.Client
|
dler downloader.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *tgFile) SetName(name string) {
|
||||||
|
f.name = name
|
||||||
|
}
|
||||||
|
|
||||||
func (f *tgFile) Location() tg.InputFileLocationClass {
|
func (f *tgFile) Location() tg.InputFileLocationClass {
|
||||||
return f.location
|
return f.location
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user