Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9367419156 | ||
|
|
f80c4d7d55 | ||
|
|
ccfde34666 | ||
|
|
2b23446123 | ||
|
|
7882185ee1 | ||
|
|
2d17a731c4 | ||
|
|
db69688722 | ||
|
|
ec09289d5f | ||
|
|
13c87debcc | ||
|
|
5f3b38c788 | ||
|
|
8ba0c623c9 | ||
|
|
6fa8e89191 | ||
|
|
3a4effab33 | ||
|
|
7692286d78 | ||
|
|
93ffc940ce | ||
|
|
4aadfc1273 | ||
|
|
d26a8df15f | ||
|
|
a746cc0fc7 | ||
|
|
0fb5634874 | ||
|
|
930e838b2e | ||
|
|
1701d1ab86 | ||
|
|
a17492d4ae | ||
|
|
a32bf43cdc |
32
README.md
32
README.md
@@ -18,6 +18,8 @@ Demo Video:
|
||||
|
||||
## 部署
|
||||
|
||||
### 从二进制文件部署
|
||||
|
||||
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
|
||||
|
||||
在解压后目录新建 `config.toml` 文件, 参考 [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
||||
@@ -29,7 +31,7 @@ chmod +x saveany-bot
|
||||
./saveany-bot
|
||||
```
|
||||
|
||||
### 添加为 systemd 服务
|
||||
#### 添加为 systemd 服务
|
||||
|
||||
创建文件 `/etc/systemd/system/saveany-bot.service` 并写入以下内容:
|
||||
|
||||
@@ -54,6 +56,27 @@ WantedBy=multi-user.target
|
||||
systemctl enable --now saveany-bot
|
||||
```
|
||||
|
||||
### 使用 Docker 部署
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
下载 [docker-compose.yml](https://github.com/krau/SaveAny-Bot/blob/main/docker-compose.yml) 文件, 并修改其中的配置.
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Docker
|
||||
|
||||
```shell
|
||||
docker run -d --name saveany-bot \
|
||||
-v /path/to/config.toml:/app/config.toml \
|
||||
-v /path/to/downloads:/app/downloads \
|
||||
ghcr.io/krau/saveany-bot:latest
|
||||
```
|
||||
|
||||
## 更新
|
||||
|
||||
使用 `upgrade` 或 `up` 升级到最新版
|
||||
@@ -62,6 +85,13 @@ systemctl enable --now saveany-bot
|
||||
./saveany-bot upgrade
|
||||
```
|
||||
|
||||
如果是 Docker 部署, 使用以下命令更新:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/krau/saveany-bot:latest
|
||||
docker restart saveany-bot
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
向 Bot 发送(转发)文件, 按照提示操作.
|
||||
|
||||
19
bot/bot.go
19
bot/bot.go
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/celestix/gotgproto/sessionMaker"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/gotd/td/telegram/dcs"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"golang.org/x/net/proxy"
|
||||
@@ -60,6 +61,24 @@ func Init() {
|
||||
Resolver: resolver,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
resultChan <- struct {
|
||||
client *gotgproto.Client
|
||||
err error
|
||||
}{nil, err}
|
||||
return
|
||||
}
|
||||
_, err = client.API().BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{
|
||||
Scope: &tg.BotCommandScopeDefault{},
|
||||
Commands: []tg.BotCommand{
|
||||
{Command: "start", Description: "开始使用"},
|
||||
{Command: "help", Description: "显示帮助"},
|
||||
{Command: "silent", Description: "开启/关闭静默模式"},
|
||||
{Command: "storage", Description: "设置默认存储端"},
|
||||
{Command: "save", Description: "保存所回复的文件"},
|
||||
{Command: "path", Description: "更改保存路径配置"},
|
||||
},
|
||||
})
|
||||
resultChan <- struct {
|
||||
client *gotgproto.Client
|
||||
err error
|
||||
|
||||
101
bot/handle_link.go
Normal file
101
bot/handle_link.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/celestix/gotgproto/dispatcher"
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/dao"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
var (
|
||||
linkRegexString = `t.me/.*/\d+`
|
||||
linkRegex = regexp.MustCompile(linkRegexString)
|
||||
)
|
||||
|
||||
func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
logger.L.Trace("Got link message")
|
||||
link := linkRegex.FindString(update.EffectiveMessage.Text)
|
||||
if link == "" {
|
||||
return dispatcher.ContinueGroups
|
||||
}
|
||||
strSlice := strings.Split(link, "/")
|
||||
if len(strSlice) < 3 {
|
||||
return dispatcher.ContinueGroups
|
||||
}
|
||||
messageID, err := strconv.Atoi(strSlice[2])
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to parse message ID: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("Failed to parse message ID"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
chatUsername := strSlice[1]
|
||||
linkChat, err := ctx.ResolveUsername(chatUsername)
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to resolve chat ID: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("Failed to resolve chat ID"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if linkChat == nil {
|
||||
logger.L.Errorf("Cannot find chat: %s", chatUsername)
|
||||
ctx.Reply(update, ext.ReplyTextString("Cannot find chat"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
user, err := dao.GetUserByUserID(update.GetUserChat().GetID())
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to get user: %s", err)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件..."), nil)
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to reply: %s", err)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
file, err := FileFromMessage(ctx, linkChat.GetID(), messageID, "")
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to get file from message: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("获取文件失败: "+err.Error()), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if file.FileName == "" {
|
||||
logger.L.Warnf("Empty file name, use generated name")
|
||||
file.FileName = fmt.Sprintf("%d_%d_%s", linkChat.GetID(), messageID, file.Hash())
|
||||
}
|
||||
|
||||
receivedFile := &types.ReceivedFile{
|
||||
Processing: false,
|
||||
FileName: file.FileName,
|
||||
ChatID: linkChat.GetID(),
|
||||
MessageID: messageID,
|
||||
ReplyMessageID: replied.ID,
|
||||
ReplyChatID: update.GetUserChat().GetID(),
|
||||
}
|
||||
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
||||
logger.L.Errorf("Failed to save received file: %s", err)
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: "无法保存文件: " + err.Error(),
|
||||
ID: replied.ID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if !user.Silent {
|
||||
return ProvideSelectMessage(ctx, update, file, int(linkChat.GetID()), messageID, replied.ID)
|
||||
}
|
||||
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||
Ctx: ctx,
|
||||
Status: types.Pending,
|
||||
File: file,
|
||||
Storage: types.StorageType(user.DefaultStorage),
|
||||
FileChatID: linkChat.GetID(),
|
||||
FileMessageID: messageID,
|
||||
ReplyMessageID: replied.ID,
|
||||
ReplyChatID: update.GetUserChat().GetID(),
|
||||
})
|
||||
}
|
||||
218
bot/handlers.go
218
bot/handlers.go
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/gookit/goutil/maputil"
|
||||
"github.com/gotd/td/telegram/message/entity"
|
||||
"github.com/gotd/td/telegram/message/styling"
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
@@ -29,6 +30,12 @@ func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
|
||||
dispatcher.AddHandler(handlers.NewCommand("silent", silent))
|
||||
dispatcher.AddHandler(handlers.NewCommand("storage", setDefaultStorage))
|
||||
dispatcher.AddHandler(handlers.NewCommand("save", saveCmd))
|
||||
dispatcher.AddHandler(handlers.NewCommand("path", setPath))
|
||||
linkRegexFilter, err := filters.Message.Regex(linkRegexString)
|
||||
if err != nil {
|
||||
logger.L.Panicf("Failed to create regex filter: %s", err)
|
||||
}
|
||||
dispatcher.AddHandler(handlers.NewMessage(linkRegexFilter, handleLinkMessage))
|
||||
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("add"), AddToQueue))
|
||||
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
|
||||
}
|
||||
@@ -56,15 +63,20 @@ func start(ctx *ext.Context, update *ext.Update) error {
|
||||
}
|
||||
|
||||
const helpText string = `
|
||||
SaveAny Bot - 转存你的 Telegram 文件
|
||||
Save Any Bot - 转存你的 Telegram 文件
|
||||
命令:
|
||||
/start - 开始使用
|
||||
/help - 显示帮助
|
||||
/silent - 静默模式
|
||||
/storage - 设置默认存储位置
|
||||
/save - 保存文件
|
||||
/save [自定义文件名] - 保存文件
|
||||
/path <存储类型> <路径> - 更改文件保存路径
|
||||
|
||||
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
|
||||
|
||||
默认存储位置: 在静默模式下保存到的位置
|
||||
|
||||
向 Bot 发送(转发)文件, 或发送一个公开频道的消息链接以保存文件
|
||||
`
|
||||
|
||||
func help(ctx *ext.Context, update *ext.Update) error {
|
||||
@@ -83,12 +95,7 @@ func silent(ctx *ext.Context, update *ext.Update) error {
|
||||
logger.L.Errorf("Failed to update user: %s", err)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已%s静默模式", func() string {
|
||||
if user.Silent {
|
||||
return "开启"
|
||||
}
|
||||
return "关闭"
|
||||
}())), nil)
|
||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已%s静默模式", map[bool]string{true: "开启", false: "关闭"}[user.Silent])), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
@@ -146,7 +153,13 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
msg, err := GetTGMessage(ctx, Client, replyToMsgID)
|
||||
|
||||
msg, err := GetTGMessage(ctx, update.EffectiveChat().GetID(), replyToMsgID)
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to get message: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("无法获取消息"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
supported, _ := supportedMediaFilter(msg)
|
||||
if !supported {
|
||||
@@ -165,74 +178,98 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
logger.L.Errorf("Failed to reply: %s", err)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
file, err := FileFromMessage(ctx, Client, update.EffectiveChat().GetID(), msg.ID)
|
||||
|
||||
cmdText := update.EffectiveMessage.Text
|
||||
customFileName := strings.TrimSpace(strings.TrimPrefix(cmdText, "/save"))
|
||||
|
||||
file, err := FileFromMessage(ctx, update.EffectiveChat().GetID(), msg.ID, customFileName)
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to get file from message: %s", err)
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: "无法获取文件",
|
||||
Message: fmt.Sprintf("获取文件失败: %s", err),
|
||||
ID: replied.ID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
if file.FileName == "" {
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: "无法获取文件名",
|
||||
ID: replied.ID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
file.FileName = fmt.Sprintf("%d_%d_%s", update.EffectiveChat().GetID(), replyToMsgID, file.Hash())
|
||||
}
|
||||
|
||||
if err := dao.AddReceivedFile(&types.ReceivedFile{
|
||||
receivedFile := &types.ReceivedFile{
|
||||
Processing: false,
|
||||
FileName: file.FileName,
|
||||
ChatID: update.EffectiveChat().GetID(),
|
||||
MessageID: replyToMsgID,
|
||||
ReplyMessageID: replied.ID,
|
||||
}); err != nil {
|
||||
logger.L.Errorf("Failed to add received file: %s", err)
|
||||
ReplyChatID: update.GetUserChat().GetID(),
|
||||
}
|
||||
|
||||
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
||||
logger.L.Errorf("Failed to save received file: %s", err)
|
||||
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: "无法保存文件",
|
||||
Message: fmt.Sprintf("Failed to save received file: %s", err),
|
||||
ID: replied.ID,
|
||||
}); err != nil {
|
||||
logger.L.Errorf("Failed to edit message: %s", err)
|
||||
}
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
if !user.Silent {
|
||||
text := "请选择存储位置"
|
||||
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
ReplyMarkup: getAddTaskMarkup(msg.ID),
|
||||
ID: replied.ID,
|
||||
})
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to reply: %s", err)
|
||||
}
|
||||
return dispatcher.EndGroups
|
||||
return ProvideSelectMessage(ctx, update, file, int(update.EffectiveChat().GetID()), msg.ID, replied.ID)
|
||||
}
|
||||
|
||||
if user.DefaultStorage == "" {
|
||||
ctx.Reply(update, ext.ReplyTextString("请先使用 /storage 设置默认存储位置"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
queue.AddTask(types.Task{
|
||||
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||
Ctx: ctx,
|
||||
Status: types.Pending,
|
||||
File: file,
|
||||
Storage: types.StorageType(user.DefaultStorage),
|
||||
ChatID: update.EffectiveChat().GetID(),
|
||||
FileChatID: update.EffectiveChat().GetID(),
|
||||
ReplyMessageID: replied.ID,
|
||||
MessageID: msg.ID,
|
||||
ReplyChatID: update.GetUserChat().GetID(),
|
||||
FileMessageID: msg.ID,
|
||||
})
|
||||
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", file.FileName, queue.Len()),
|
||||
ID: replied.ID,
|
||||
})
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to edit message: %s", err)
|
||||
}
|
||||
|
||||
func setPath(ctx *ext.Context, update *ext.Update) error {
|
||||
if len(storage.Storages) == 0 {
|
||||
ctx.Reply(update, ext.ReplyTextString("未配置存储"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if update.EffectiveMessage == nil {
|
||||
logger.L.Error("No effective message")
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||
if len(args) < 3 {
|
||||
text := []styling.StyledTextOption{
|
||||
styling.Plain("请提供存储位置名称和路径, 可用项:"),
|
||||
}
|
||||
for name := range storage.Storages {
|
||||
text = append(text, styling.Plain("\n"))
|
||||
text = append(text, styling.Code(string(name)))
|
||||
}
|
||||
text = append(text, styling.Plain("\n示例: /path local /path/to/save"))
|
||||
ctx.Reply(update, ext.ReplyTextStyledTextArray(text), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
storageName := args[1]
|
||||
if _, ok := storage.Storages[types.StorageType(storageName)]; !ok {
|
||||
ctx.Reply(update, ext.ReplyTextString("存储位置不存在"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
path := strings.Join(args[2:], " ")
|
||||
switch storageName {
|
||||
case "local":
|
||||
config.Set("storage.local.base_path", path)
|
||||
case "webdav":
|
||||
config.Set("storage.webdav.base_path", path)
|
||||
case "alist":
|
||||
config.Set("storage.alist.base_path", path)
|
||||
}
|
||||
if err := config.ReloadConfig(); err != nil {
|
||||
logger.L.Errorf("Failed to reload config: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("设置失败: "+err.Error()), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextString("设置成功"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
@@ -258,71 +295,47 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
media := update.EffectiveMessage.Media
|
||||
file, err := FileFromMedia(media)
|
||||
file, err := FileFromMedia(media, "")
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to get file from media: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("无法获取文件"), nil)
|
||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if file.FileName == "" {
|
||||
ctx.Reply(update, ext.ReplyTextString("无法获取文件名"), nil)
|
||||
return dispatcher.EndGroups
|
||||
file.FileName = fmt.Sprintf("%d_%d_%s", update.EffectiveChat().GetID(), update.EffectiveMessage.ID, file.Hash())
|
||||
}
|
||||
|
||||
if err := dao.AddReceivedFile(&types.ReceivedFile{
|
||||
if err := dao.SaveReceivedFile(&types.ReceivedFile{
|
||||
Processing: false,
|
||||
FileName: file.FileName,
|
||||
ChatID: update.EffectiveChat().GetID(),
|
||||
MessageID: update.EffectiveMessage.ID,
|
||||
ReplyMessageID: msg.ID,
|
||||
ReplyChatID: update.GetUserChat().GetID(),
|
||||
}); err != nil {
|
||||
logger.L.Errorf("Failed to add received file: %s", err)
|
||||
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: "无法保存文件",
|
||||
Message: fmt.Sprintf("Failed to add received file: %s", err),
|
||||
ID: msg.ID,
|
||||
}); err != nil {
|
||||
logger.L.Errorf("Failed to edit message: %s", err)
|
||||
}
|
||||
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
if !user.Silent {
|
||||
text := "请选择存储位置"
|
||||
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
ReplyMarkup: getAddTaskMarkup(update.EffectiveMessage.ID),
|
||||
ID: msg.ID,
|
||||
})
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to edit message: %s", err)
|
||||
}
|
||||
return dispatcher.EndGroups
|
||||
return ProvideSelectMessage(ctx, update, file, int(update.EffectiveChat().GetID()), update.EffectiveMessage.ID, msg.ID)
|
||||
}
|
||||
|
||||
if user.DefaultStorage == "" {
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: "请先使用 /storage 设置默认存储位置",
|
||||
ID: msg.ID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
queue.AddTask(types.Task{
|
||||
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||
Ctx: ctx,
|
||||
Status: types.Pending,
|
||||
File: file,
|
||||
Storage: types.StorageType(user.DefaultStorage),
|
||||
ChatID: update.EffectiveChat().GetID(),
|
||||
FileChatID: update.EffectiveChat().GetID(),
|
||||
ReplyMessageID: msg.ID,
|
||||
MessageID: update.EffectiveMessage.ID,
|
||||
ReplyChatID: update.GetUserChat().GetID(),
|
||||
FileMessageID: update.EffectiveMessage.ID,
|
||||
})
|
||||
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", file.FileName, queue.Len()),
|
||||
ID: msg.ID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
||||
@@ -336,9 +349,11 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
args := strings.Split(string(update.CallbackQuery.Data), " ")
|
||||
messageID, _ := strconv.Atoi(args[1])
|
||||
logger.L.Tracef("Got add to queue: chatID: %d, messageID: %d, storage: %s", update.EffectiveChat().GetID(), messageID, args[2])
|
||||
record, err := dao.GetReceivedFileByChatAndMessageID(update.EffectiveChat().GetID(), messageID)
|
||||
chatID, _ := strconv.Atoi(args[1])
|
||||
messageID, _ := strconv.Atoi(args[2])
|
||||
storageName := args[3]
|
||||
logger.L.Tracef("Got add to queue: chatID: %d, messageID: %d, storage: %s", chatID, messageID, storageName)
|
||||
record, err := dao.GetReceivedFileByChatAndMessageID(int64(chatID), messageID)
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to get received file: %s", err)
|
||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||
@@ -351,18 +366,18 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
||||
}
|
||||
if update.CallbackQuery.MsgID != record.ReplyMessageID {
|
||||
record.ReplyMessageID = update.CallbackQuery.MsgID
|
||||
if err := dao.UpdateReceivedFile(record); err != nil {
|
||||
if err := dao.SaveReceivedFile(record); err != nil {
|
||||
logger.L.Errorf("Failed to update received file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
file, err := FileFromMessage(ctx, Client, record.ChatID, record.MessageID)
|
||||
file, err := FileFromMessage(ctx, record.ChatID, record.MessageID, record.FileName)
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to get file from message: %s", err)
|
||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||
QueryID: update.CallbackQuery.QueryID,
|
||||
Alert: true,
|
||||
Message: "获取消息文件失败",
|
||||
Message: fmt.Sprintf("获取消息中的文件失败: %s", err),
|
||||
CacheTime: 5,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
@@ -372,14 +387,31 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
||||
Ctx: ctx,
|
||||
Status: types.Pending,
|
||||
File: file,
|
||||
Storage: types.StorageType(args[2]),
|
||||
ChatID: record.ChatID,
|
||||
Storage: types.StorageType(storageName),
|
||||
FileChatID: record.ChatID,
|
||||
ReplyMessageID: record.ReplyMessageID,
|
||||
MessageID: record.MessageID,
|
||||
FileMessageID: record.MessageID,
|
||||
ReplyChatID: record.ReplyChatID,
|
||||
})
|
||||
|
||||
entityBuilder := entity.Builder{}
|
||||
var entities []tg.MessageEntityClass
|
||||
text := fmt.Sprintf("已添加到任务队列\n文件名: %s\n当前排队任务数: %d", record.FileName, queue.Len())
|
||||
if err := styling.Perform(&entityBuilder,
|
||||
styling.Plain("已添加到任务队列\n文件名: "),
|
||||
styling.Code(record.FileName),
|
||||
styling.Plain("\n当前排队任务数: "),
|
||||
styling.Bold(strconv.Itoa(queue.Len())),
|
||||
); err != nil {
|
||||
logger.L.Errorf("Failed to build entity: %s", err)
|
||||
} else {
|
||||
text, entities = entityBuilder.Complete()
|
||||
}
|
||||
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", record.FileName, queue.Len()),
|
||||
ID: record.ReplyMessageID,
|
||||
Message: text,
|
||||
Entities: entities,
|
||||
ID: record.ReplyMessageID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
119
bot/utils.go
119
bot/utils.go
@@ -1,20 +1,29 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/celestix/gotgproto"
|
||||
"github.com/celestix/gotgproto/dispatcher"
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/gotd/td/telegram/message/entity"
|
||||
"github.com/gotd/td/telegram/message/styling"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/queue"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyDocument = errors.New("document is empty")
|
||||
ErrEmptyPhoto = errors.New("photo is empty")
|
||||
ErrEmptyPhotoSize = errors.New("photo size is empty")
|
||||
ErrEmptyPhotoSizes = errors.New("photo size slice is empty")
|
||||
)
|
||||
|
||||
func supportedMediaFilter(m *tg.Message) (bool, error) {
|
||||
if not := m.Media == nil; not {
|
||||
return false, dispatcher.EndGroups
|
||||
@@ -36,12 +45,12 @@ var StorageDisplayNames = map[string]string{
|
||||
"webdav": "WebDAV",
|
||||
}
|
||||
|
||||
func getAddTaskMarkup(messageID int) *tg.ReplyInlineMarkup {
|
||||
func getAddTaskMarkup(chatID, messageID int) *tg.ReplyInlineMarkup {
|
||||
storageButtons := make([]tg.KeyboardButtonClass, 0)
|
||||
for _, name := range storage.StorageKeys {
|
||||
storageButtons = append(storageButtons, &tg.KeyboardButtonCallback{
|
||||
Text: StorageDisplayNames[string(name)],
|
||||
Data: []byte(fmt.Sprintf("add %d %s", messageID, name)),
|
||||
Data: []byte(fmt.Sprintf("add %d %d %s", chatID, messageID, name)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,7 +75,7 @@ func getAddTaskMarkup(messageID int) *tg.ReplyInlineMarkup {
|
||||
Buttons: []tg.KeyboardButtonClass{
|
||||
&tg.KeyboardButtonCallback{
|
||||
Text: "全部",
|
||||
Data: []byte(fmt.Sprintf("add %d all", messageID)),
|
||||
Data: []byte(fmt.Sprintf("add %d %d all", chatID, messageID)),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -74,24 +83,27 @@ func getAddTaskMarkup(messageID int) *tg.ReplyInlineMarkup {
|
||||
}
|
||||
}
|
||||
|
||||
func FileFromMedia(media tg.MessageMediaClass) (*types.File, error) {
|
||||
func FileFromMedia(media tg.MessageMediaClass, customFileName string) (*types.File, error) {
|
||||
switch media := media.(type) {
|
||||
case *tg.MessageMediaDocument:
|
||||
document, ok := media.Document.AsNotEmpty()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("document is empty")
|
||||
return nil, ErrEmptyDocument
|
||||
}
|
||||
var fileName string
|
||||
if customFileName != "" {
|
||||
return &types.File{
|
||||
Location: document.AsInputDocumentFileLocation(),
|
||||
FileSize: document.Size,
|
||||
FileName: customFileName,
|
||||
}, nil
|
||||
}
|
||||
fileName := ""
|
||||
for _, attribute := range document.Attributes {
|
||||
if name, ok := attribute.(*tg.DocumentAttributeFilename); ok {
|
||||
fileName = name.GetFileName()
|
||||
break
|
||||
}
|
||||
}
|
||||
if fileName == "" {
|
||||
fileName = fmt.Sprintf("%x", md5.Sum(document.GetFileReference()))
|
||||
logger.L.Warnf("File name is empty, using hash: %s", fileName)
|
||||
}
|
||||
return &types.File{
|
||||
Location: document.AsInputDocumentFileLocation(),
|
||||
FileSize: document.Size,
|
||||
@@ -100,33 +112,37 @@ func FileFromMedia(media tg.MessageMediaClass) (*types.File, error) {
|
||||
case *tg.MessageMediaPhoto:
|
||||
photo, ok := media.Photo.AsNotEmpty()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("photo is empty")
|
||||
return nil, ErrEmptyPhoto
|
||||
}
|
||||
sizes := photo.Sizes
|
||||
if len(sizes) == 0 {
|
||||
return nil, fmt.Errorf("photo sizes is empty")
|
||||
return nil, ErrEmptyPhotoSizes
|
||||
}
|
||||
photoSize := sizes[len(sizes)-1]
|
||||
size, ok := photoSize.AsNotEmpty()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("photo size is empty")
|
||||
return nil, ErrEmptyPhotoSize
|
||||
}
|
||||
location := new(tg.InputPhotoFileLocation)
|
||||
location.ID = photo.GetID()
|
||||
location.AccessHash = photo.GetAccessHash()
|
||||
location.FileReference = photo.GetFileReference()
|
||||
location.ThumbSize = size.GetType()
|
||||
fileName := customFileName
|
||||
if fileName == "" {
|
||||
fileName = fmt.Sprintf("photo_%s_%d.jpg", time.Now().Format("2006-01-02_15-04-05"), photo.GetID())
|
||||
}
|
||||
return &types.File{
|
||||
Location: location,
|
||||
FileSize: 0,
|
||||
FileName: fmt.Sprintf("photo_%s_%d.jpg", time.Now().Format("2006-01-02_15-04-05"), photo.GetID()),
|
||||
FileName: fileName,
|
||||
}, nil
|
||||
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected type %T", media)
|
||||
}
|
||||
|
||||
func FileFromMessage(ctx context.Context, client *gotgproto.Client, chatID int64, messageID int) (*types.File, error) {
|
||||
func FileFromMessage(ctx *ext.Context, chatID int64, messageID int, customFileName string) (*types.File, error) {
|
||||
key := fmt.Sprintf("file:%d:%d", chatID, messageID)
|
||||
logger.L.Debugf("Getting file: %s", key)
|
||||
var cachedFile types.File
|
||||
@@ -134,12 +150,11 @@ func FileFromMessage(ctx context.Context, client *gotgproto.Client, chatID int64
|
||||
if err == nil {
|
||||
return &cachedFile, nil
|
||||
}
|
||||
|
||||
message, err := GetTGMessage(ctx, client, messageID)
|
||||
message, err := GetTGMessage(ctx, chatID, messageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := FileFromMedia(message.Media)
|
||||
file, err := FileFromMedia(message.Media, customFileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -149,20 +164,60 @@ func FileFromMessage(ctx context.Context, client *gotgproto.Client, chatID int64
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func GetTGMessage(ctx context.Context, client *gotgproto.Client, messageID int) (*tg.Message, error) {
|
||||
func GetTGMessage(ctx *ext.Context, chatId int64, messageID int) (*tg.Message, error) {
|
||||
logger.L.Debugf("Fetching message: %d", messageID)
|
||||
res, err := client.API().MessagesGetMessages(ctx, []tg.InputMessageClass{
|
||||
&tg.InputMessageID{
|
||||
ID: messageID,
|
||||
},
|
||||
})
|
||||
messages, err := ctx.GetMessages(chatId, []tg.InputMessageClass{&tg.InputMessageID{ID: messageID}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
messages := res.(*tg.MessagesMessages)
|
||||
msg := messages.Messages[0]
|
||||
if _, ok := msg.(*tg.Message); !ok {
|
||||
return nil, fmt.Errorf("unexpected type %T, this file may be deleted", msg)
|
||||
if len(messages) == 0 {
|
||||
return nil, errors.New("no messages found")
|
||||
}
|
||||
return msg.(*tg.Message), nil
|
||||
msg := messages[0]
|
||||
tgMessage, ok := msg.(*tg.Message)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected message type: %T", msg)
|
||||
}
|
||||
return tgMessage, nil
|
||||
}
|
||||
|
||||
func ProvideSelectMessage(ctx *ext.Context, update *ext.Update, file *types.File, chatID int, fileMsgID, toEditMsgID int) error {
|
||||
entityBuilder := entity.Builder{}
|
||||
var entities []tg.MessageEntityClass
|
||||
text := fmt.Sprintf("文件名: %s\n请选择存储位置", file.FileName)
|
||||
if err := styling.Perform(&entityBuilder,
|
||||
styling.Plain("文件名: "),
|
||||
styling.Code(file.FileName),
|
||||
styling.Plain("\n请选择存储位置"),
|
||||
); err != nil {
|
||||
logger.L.Errorf("Failed to build entity: %s", err)
|
||||
} else {
|
||||
text, entities = entityBuilder.Complete()
|
||||
}
|
||||
_, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
Entities: entities,
|
||||
ReplyMarkup: getAddTaskMarkup(chatID, fileMsgID),
|
||||
ID: toEditMsgID,
|
||||
})
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to reply: %s", err)
|
||||
}
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
func HandleSilentAddTask(ctx *ext.Context, update *ext.Update, user *types.User, task *types.Task) error {
|
||||
if user.DefaultStorage == "" {
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: "请先使用 /storage 设置默认存储位置",
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
queue.AddTask(*task)
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", task.FileName(), queue.Len()),
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
31
cmd/run.go
31
cmd/run.go
@@ -3,9 +3,11 @@ package cmd
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/bootstrap"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/core"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -18,5 +20,32 @@ func Run(_ *cobra.Command, _ []string) {
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
sig := <-quit
|
||||
logger.L.Info(sig, ", exit")
|
||||
logger.L.Info(sig, ", exitting...")
|
||||
defer logger.L.Info("Bye!")
|
||||
if config.Cfg.NoCleanCache {
|
||||
return
|
||||
}
|
||||
if config.Cfg.Temp.BasePath != "" {
|
||||
for _, path := range []string{"/", ".", "\\", ".."} {
|
||||
if filepath.Clean(config.Cfg.Temp.BasePath) == path {
|
||||
logger.L.Error("Invalid cache dir: ", config.Cfg.Temp.BasePath)
|
||||
return
|
||||
}
|
||||
}
|
||||
currentDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
logger.L.Error("Failed to get current dir: ", err)
|
||||
return
|
||||
}
|
||||
cachePath := filepath.Join(currentDir, config.Cfg.Temp.BasePath)
|
||||
cachePath, err = filepath.Abs(cachePath)
|
||||
if err != nil {
|
||||
logger.L.Error("Failed to get absolute path: ", err)
|
||||
return
|
||||
}
|
||||
logger.L.Info("Cleaning cache dir: ", cachePath)
|
||||
if err := os.RemoveAll(cachePath); err != nil {
|
||||
logger.L.Error("Failed to clean cache dir: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,20 @@ workers = 4 # 同时下载文件数
|
||||
retry = 3 # 下载失败重试次数
|
||||
|
||||
[telegram]
|
||||
token = "" # Bot Token
|
||||
admins = [777000] # 你的 user_id
|
||||
app_id = 123456 # Telegram API ID
|
||||
app_hash = "0123456789abcdef0123456789abcdef" # Telegram API Hash
|
||||
# Bot Token
|
||||
token = ""
|
||||
# 允许使用的用户 id 列表
|
||||
admins = [777000]
|
||||
# Telegram API 配置, 若不配置也可运行, 将使用默认的 API ID 和 API HASH
|
||||
# 推荐使用自己的 API ID 和 API HASH (https://my.telegram.org)
|
||||
# app_id = 123456
|
||||
# app_hash = "0123456789abcdef0123456789abcdef"
|
||||
|
||||
[telegram.proxy]
|
||||
# 启用代理连接 telegram, 只支持 socks5
|
||||
enable = false
|
||||
url = "socks5://127.0.0.1:7890" # 代理地址
|
||||
url = "socks5://127.0.0.1:7890"
|
||||
|
||||
[log]
|
||||
level = "DEBUG" # 日志等级
|
||||
|
||||
[temp]
|
||||
base_path = "cache/" # 下载文件临时目录, 请不要在此目录下存放任何其他文件
|
||||
cache_ttl = 30 # 临时文件保存时间, 单位: 秒
|
||||
|
||||
[db]
|
||||
path = "data/data.db" # 数据库文件路径
|
||||
|
||||
[storage]
|
||||
[storage.alist] # Alist
|
||||
@@ -29,6 +25,9 @@ username = "admin" # 用户名
|
||||
password = "password" # 密码
|
||||
url = "https://alist.com" # Alist 地址
|
||||
token_exp = 86400 # token 过期时间, 单位: 秒
|
||||
# 可直接使用 token 授权, 此时不能自动刷新登录信息
|
||||
# 配置 token 后, username , password , token_exp 将被忽略
|
||||
token = "jwt_token"
|
||||
|
||||
[storage.local] # 本地磁盘
|
||||
enable = true
|
||||
@@ -40,3 +39,15 @@ base_path = "/telegram"
|
||||
username = "admin"
|
||||
password = "password"
|
||||
url = "https://alist.com/dav"
|
||||
|
||||
|
||||
[log]
|
||||
# 日志等级
|
||||
level = "DEBUG"
|
||||
|
||||
[temp]
|
||||
base_path = "cache/" # 下载文件临时目录, 请不要在此目录下存放任何其他文件
|
||||
cache_ttl = 30 # 临时文件保存时间, 单位: 秒
|
||||
|
||||
[db]
|
||||
path = "data/data.db" # 数据库文件路径
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Workers int `toml:"workers" mapstructure:"workers"`
|
||||
Retry int `toml:"retry" mapstructure:"retry"`
|
||||
Workers int `toml:"workers" mapstructure:"workers"`
|
||||
Retry int `toml:"retry" mapstructure:"retry"`
|
||||
NoCleanCache bool `toml:"no_clean_cache" mapstructure:"no_clean_cache"`
|
||||
|
||||
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
||||
Log logConfig `toml:"log" mapstructure:"log"`
|
||||
@@ -58,6 +59,7 @@ type alistConfig struct {
|
||||
URL string `toml:"url" mapstructure:"url"`
|
||||
Username string `toml:"username" mapstructure:"username"`
|
||||
Password string `toml:"password" mapstructure:"password"`
|
||||
Token string `toml:"token" mapstructure:"token"`
|
||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
||||
TokenExp int64 `toml:"token_exp" mapstructure:"token_exp"`
|
||||
}
|
||||
@@ -90,6 +92,9 @@ func Init() {
|
||||
viper.SetDefault("workers", 3)
|
||||
viper.SetDefault("retry", 3)
|
||||
|
||||
viper.SetDefault("telegram.app_id", 1025907)
|
||||
viper.SetDefault("telegram.app_hash", "452b0359b988148995f22ff0f4229750")
|
||||
|
||||
viper.SetDefault("temp.base_path", "cache/")
|
||||
viper.SetDefault("temp.cache_ttl", 3600)
|
||||
|
||||
@@ -102,6 +107,8 @@ func Init() {
|
||||
viper.SetDefault("storage.alist.base_path", "/")
|
||||
viper.SetDefault("storage.alist.token_exp", 3600)
|
||||
|
||||
viper.SafeWriteConfigAs("config.toml")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
fmt.Println("Error reading config file, ", err)
|
||||
os.Exit(1)
|
||||
@@ -117,3 +124,20 @@ func Init() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func Set(key string, value any) {
|
||||
viper.Set(key, value)
|
||||
}
|
||||
|
||||
func ReloadConfig() error {
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if error := viper.Unmarshal(Cfg); error != nil {
|
||||
return error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
117
core/core.go
117
core/core.go
@@ -6,9 +6,14 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/duke-git/lancet/v2/fileutil"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/bot"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
@@ -19,46 +24,36 @@ import (
|
||||
|
||||
func processPendingTask(task *types.Task) error {
|
||||
logger.L.Debugf("Start processing task: %s", task.String())
|
||||
os.MkdirAll(config.Cfg.Temp.BasePath, os.ModePerm)
|
||||
if task.FileName() == "" {
|
||||
task.File.FileName = fmt.Sprintf("%d_%d_%s", task.FileChatID, task.FileMessageID, task.File.Hash())
|
||||
}
|
||||
cacheDestPath := filepath.Join(config.Cfg.Temp.BasePath, task.FileName())
|
||||
cacheDestPath, err := filepath.Abs(cacheDestPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
if err := fileutil.CreateDir(filepath.Dir(cacheDestPath)); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
ctx := task.Ctx.(*ext.Context)
|
||||
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: "正在下载: " + task.String(),
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
|
||||
destPath := filepath.Join(config.Cfg.Temp.BasePath, task.File.FileName)
|
||||
if task.StoragePath == "" {
|
||||
task.StoragePath = task.File.FileName
|
||||
}
|
||||
|
||||
// process photo
|
||||
if task.File.FileSize == 0 {
|
||||
res, err := bot.Client.API().UploadGetFile(task.Ctx, &tg.UploadGetFileRequest{
|
||||
Location: task.File.Location,
|
||||
Offset: 0,
|
||||
Limit: 1024 * 1024,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get file: %w", err)
|
||||
}
|
||||
|
||||
result, ok := res.(*tg.UploadFile)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T", res)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(destPath, result.Bytes, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("Failed to write file: %w", err)
|
||||
}
|
||||
|
||||
defer cleanCacheFile(destPath)
|
||||
|
||||
logger.L.Infof("Downloaded file: %s", destPath)
|
||||
|
||||
return saveFileWithRetry(task, destPath)
|
||||
switch task.Storage {
|
||||
case types.Local:
|
||||
task.StoragePath = filepath.Join(config.Cfg.Storage.Local.BasePath, task.StoragePath)
|
||||
case types.Webdav:
|
||||
task.StoragePath = path.Join(config.Cfg.Storage.Webdav.BasePath, task.StoragePath)
|
||||
case types.Alist:
|
||||
task.StoragePath = path.Join(config.Cfg.Storage.Alist.BasePath, task.StoragePath)
|
||||
}
|
||||
|
||||
if task.File.FileSize == 0 {
|
||||
return processPhoto(task, cacheDestPath)
|
||||
}
|
||||
|
||||
ctx := task.Ctx.(*ext.Context)
|
||||
|
||||
barTotalCount := calculateBarTotalCount(task.File.FileSize)
|
||||
|
||||
progressCallback := func(bytesRead, contentLength int64) {
|
||||
@@ -67,44 +62,56 @@ func processPendingTask(task *types.Task) error {
|
||||
if task.File.FileSize < 1024*1024*50 || int(progress)%(100/barTotalCount) != 0 {
|
||||
return
|
||||
}
|
||||
text := fmt.Sprintf("正在下载: %s\n[%s] %.2f%%",
|
||||
task.String(),
|
||||
getProgressBar(progress, barTotalCount),
|
||||
progress,
|
||||
)
|
||||
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
ID: task.ReplyMessageID,
|
||||
text, entities := buildProgressMessageEntity(task, barTotalCount, bytesRead, task.StartTime, progress)
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
Entities: entities,
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
}
|
||||
|
||||
text, entities := buildProgressMessageEntity(task, barTotalCount, 0, task.StartTime, 0)
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
Entities: entities,
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
|
||||
readCloser, err := NewTelegramReader(task.Ctx, bot.Client, &task.File.Location,
|
||||
0, task.File.FileSize-1, task.File.FileSize,
|
||||
progressCallback, task.File.FileSize/100)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create reader: %w", err)
|
||||
return fmt.Errorf("failed to create reader: %w", err)
|
||||
}
|
||||
defer readCloser.Close()
|
||||
|
||||
dest, err := os.Create(destPath)
|
||||
dest, err := os.Create(cacheDestPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create file: %w", err)
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer dest.Close()
|
||||
|
||||
task.StartTime = time.Now()
|
||||
if _, err := io.CopyN(dest, readCloser, task.File.FileSize); err != nil {
|
||||
return fmt.Errorf("Failed to download file: %w", err)
|
||||
return fmt.Errorf("failed to download file: %w", err)
|
||||
}
|
||||
defer cleanCacheFile(cacheDestPath)
|
||||
if path.Ext(task.FileName()) == "" {
|
||||
mimeType, err := mimetype.DetectFile(cacheDestPath)
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to detect mime type: %s", err)
|
||||
} else {
|
||||
task.File.FileName = fmt.Sprintf("%s%s", task.FileName(), mimeType.Extension())
|
||||
task.StoragePath = fmt.Sprintf("%s%s", task.StoragePath, mimeType.Extension())
|
||||
}
|
||||
}
|
||||
|
||||
defer cleanCacheFile(destPath)
|
||||
|
||||
logger.L.Infof("Downloaded file: %s", destPath)
|
||||
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
||||
logger.L.Infof("Downloaded file: %s", cacheDestPath)
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()),
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
|
||||
return saveFileWithRetry(task, destPath)
|
||||
return saveFileWithRetry(task, cacheDestPath)
|
||||
}
|
||||
|
||||
func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
||||
@@ -131,13 +138,13 @@ func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
||||
queue.AddTask(task)
|
||||
case types.Succeeded:
|
||||
logger.L.Infof("Task succeeded: %s", task.String())
|
||||
task.Ctx.(*ext.Context).EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: "保存成功\n" + task.FileName(),
|
||||
task.Ctx.(*ext.Context).EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("文件保存成功\n [%s]: %s", task.Storage, task.StoragePath),
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
case types.Failed:
|
||||
logger.L.Errorf("Task failed: %s", task.String())
|
||||
task.Ctx.(*ext.Context).EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
||||
task.Ctx.(*ext.Context).EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: "文件保存失败\n" + task.Error.Error(),
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
|
||||
@@ -5,6 +5,10 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/telegram/message/entity"
|
||||
"github.com/gotd/td/telegram/message/styling"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/bot"
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
@@ -12,11 +16,11 @@ import (
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
func saveFileWithRetry(task *types.Task, destPath string) error {
|
||||
func saveFileWithRetry(task *types.Task, localFilePath string) error {
|
||||
for i := 0; i <= config.Cfg.Retry; i++ {
|
||||
if err := storage.Save(task.Storage, task.Ctx, destPath, task.StoragePath); err != nil {
|
||||
if err := storage.Save(task.Storage, task.Ctx, localFilePath, task.StoragePath); err != nil {
|
||||
if i == config.Cfg.Retry {
|
||||
return fmt.Errorf("Failed to save file: %w", err)
|
||||
return fmt.Errorf("failed to save file: %w", err)
|
||||
}
|
||||
logger.L.Errorf("Failed to save file: %s, retrying...", err)
|
||||
continue
|
||||
@@ -26,6 +30,32 @@ func saveFileWithRetry(task *types.Task, destPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func processPhoto(task *types.Task, cachePath string) error {
|
||||
res, err := bot.Client.API().UploadGetFile(task.Ctx, &tg.UploadGetFileRequest{
|
||||
Location: task.File.Location,
|
||||
Offset: 0,
|
||||
Limit: 1024 * 1024,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
|
||||
result, ok := res.(*tg.UploadFile)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T", res)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cachePath, result.Bytes, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
|
||||
defer cleanCacheFile(cachePath)
|
||||
|
||||
logger.L.Infof("Downloaded file: %s", cachePath)
|
||||
|
||||
return saveFileWithRetry(task, cachePath)
|
||||
}
|
||||
|
||||
func getProgressBar(progress float64, totalCount int) string {
|
||||
bar := ""
|
||||
barSize := 100 / totalCount
|
||||
@@ -52,7 +82,7 @@ func cleanCacheFile(destPath string) {
|
||||
func calculateBarTotalCount(fileSize int64) int {
|
||||
barTotalCount := 5
|
||||
if fileSize > 1024*1024*1000 {
|
||||
barTotalCount = 50
|
||||
barTotalCount = 40
|
||||
} else if fileSize > 1024*1024*500 {
|
||||
barTotalCount = 20
|
||||
} else if fileSize > 1024*1024*200 {
|
||||
@@ -60,3 +90,38 @@ func calculateBarTotalCount(fileSize int64) int {
|
||||
}
|
||||
return barTotalCount
|
||||
}
|
||||
|
||||
func getSpeed(bytesRead int64, startTime time.Time) string {
|
||||
if startTime.IsZero() {
|
||||
return "0MB/s"
|
||||
}
|
||||
elapsed := time.Since(startTime)
|
||||
speed := float64(bytesRead) / 1024 / 1024 / elapsed.Seconds()
|
||||
return fmt.Sprintf("%.2fMB/s", speed)
|
||||
}
|
||||
|
||||
func buildProgressMessageEntity(task *types.Task, barTotalCount int, bytesRead int64, startTime time.Time, progress float64) (string, []tg.MessageEntityClass) {
|
||||
entityBuilder := entity.Builder{}
|
||||
text := fmt.Sprintf("正在处理下载任务\n文件名: %s\n保存路径: %s\n平均速度: %s\n当前进度: [%s] %.2f%%",
|
||||
task.FileName(),
|
||||
fmt.Sprintf("[%s]:%s", task.Storage, task.StoragePath),
|
||||
getSpeed(bytesRead, startTime),
|
||||
getProgressBar(progress, barTotalCount),
|
||||
progress,
|
||||
)
|
||||
var entities []tg.MessageEntityClass
|
||||
if err := styling.Perform(&entityBuilder,
|
||||
styling.Plain("正在处理下载任务\n文件名: "),
|
||||
styling.Code(task.FileName()),
|
||||
styling.Plain("\n保存路径: "),
|
||||
styling.Code(fmt.Sprintf("[%s]:%s", task.Storage, task.StoragePath)),
|
||||
styling.Plain("\n平均速度: "),
|
||||
styling.Bold(getSpeed(bytesRead, task.StartTime)),
|
||||
styling.Plain("\n当前进度:\n "),
|
||||
styling.Code(fmt.Sprintf("[%s] %.2f%%", getProgressBar(progress, barTotalCount), progress)),
|
||||
); err != nil {
|
||||
logger.L.Errorf("Failed to build entities: %s", err)
|
||||
return text, entities
|
||||
}
|
||||
return entityBuilder.Complete()
|
||||
}
|
||||
|
||||
13
dao/db.go
13
dao/db.go
@@ -3,12 +3,14 @@ package dao
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
"gorm.io/gorm"
|
||||
glogger "gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
@@ -19,7 +21,16 @@ func Init() {
|
||||
os.Exit(1)
|
||||
}
|
||||
var err error
|
||||
db, err = gorm.Open(sqlite.Open(config.Cfg.DB.Path), &gorm.Config{})
|
||||
db, err = gorm.Open(sqlite.Open(config.Cfg.DB.Path), &gorm.Config{
|
||||
Logger: glogger.New(logger.L, glogger.Config{
|
||||
Colorful: true,
|
||||
SlowThreshold: time.Second * 5,
|
||||
LogLevel: glogger.Error,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
ParameterizedQueries: true,
|
||||
}),
|
||||
PrepareStmt: true,
|
||||
})
|
||||
if err != nil {
|
||||
logger.L.Fatal("Failed to open database: ", err)
|
||||
os.Exit(1)
|
||||
|
||||
12
dao/file.go
12
dao/file.go
@@ -2,8 +2,12 @@ package dao
|
||||
|
||||
import "github.com/krau/SaveAny-Bot/types"
|
||||
|
||||
func AddReceivedFile(receivedFile *types.ReceivedFile) error {
|
||||
return db.Create(receivedFile).Error
|
||||
func SaveReceivedFile(receivedFile *types.ReceivedFile) error {
|
||||
record, err := GetReceivedFileByChatAndMessageID(receivedFile.ChatID, receivedFile.MessageID)
|
||||
if err == nil {
|
||||
receivedFile.ID = record.ID
|
||||
}
|
||||
return db.Save(receivedFile).Error
|
||||
}
|
||||
|
||||
func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*types.ReceivedFile, error) {
|
||||
@@ -15,10 +19,6 @@ func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*types.Rece
|
||||
return &receivedFile, nil
|
||||
}
|
||||
|
||||
func UpdateReceivedFile(receivedFile *types.ReceivedFile) error {
|
||||
return db.Save(receivedFile).Error
|
||||
}
|
||||
|
||||
func DeleteReceivedFile(receivedFile *types.ReceivedFile) error {
|
||||
return db.Delete(receivedFile).Error
|
||||
}
|
||||
|
||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
saveany-bot:
|
||||
image: ghcr.io/krau/saveany-bot:latest
|
||||
container_name: saveany-bot
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SAVEANY_TELEGRAM_TOKEN=bot_token
|
||||
- SAVEANY_TELEGRAM_ADMINS=admin_id1,admin_id2
|
||||
# 推荐使用自己的 API ID 和 API HASH (https://my.telegram.org)
|
||||
# 若不配置也可运行, 将使用默认的 API ID 和 API HASH
|
||||
# - SAVEANY_TELEGRAM_APP_ID=app_id
|
||||
# - SAVEANY_TELEGRAM_APP_HASH=app_hash
|
||||
|
||||
# 本地存储
|
||||
- SAVEANY_STORAGE_LOCAL_ENABLE=true
|
||||
- SAVEANY_STORAGE_LOCAL_BASE_PATH=/app/downloads
|
||||
# Alist
|
||||
- SAVEANY_STORAGE_ALIST_ENABLE=true
|
||||
- SAVEANY_STORAGE_ALIST_BASE_PATH=/saveany
|
||||
- SAVEANY_STORAGE_ALIST_URL=https://example.com
|
||||
- SAVEANY_STORAGE_ALIST_USERNAME=username
|
||||
- SAVEANY_STORAGE_ALIST_PASSWORD=password
|
||||
# webdav
|
||||
- SAVEANY_STORAGE_WEBDAV_ENABLE=true
|
||||
- SAVEANY_STORAGE_WEBDAV_BASE_PATH=/saveany
|
||||
- SAVEANY_STORAGE_WEBDAV_URL=https://example.com
|
||||
- SAVEANY_STORAGE_WEBDAV_USERNAME=username
|
||||
- SAVEANY_STORAGE_WEBDAV_PASSWORD=password
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./downloads:/app/downloads
|
||||
- ./cache:/app/cache
|
||||
33
go.mod
33
go.mod
@@ -4,16 +4,16 @@ go 1.23.5
|
||||
|
||||
require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/celestix/gotgproto v1.0.0-beta20.1
|
||||
github.com/celestix/gotgproto v1.0.0-beta20.2
|
||||
github.com/gookit/slog v0.5.7
|
||||
github.com/gotd/contrib v0.21.0
|
||||
github.com/gotd/td v0.118.0
|
||||
github.com/gotd/td v0.120.0
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/studio-b12/gowebdav v0.10.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/time v0.9.0
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/time v0.10.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -21,9 +21,10 @@ require (
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
@@ -42,7 +43,7 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/ogen-go/ogen v1.9.0 // indirect
|
||||
github.com/ogen-go/ogen v1.10.0 // indirect
|
||||
github.com/onsi/gomega v1.36.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
@@ -54,15 +55,15 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/oauth2 v0.25.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
modernc.org/libc v1.61.11 // indirect
|
||||
modernc.org/libc v1.61.13 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.8.2 // indirect
|
||||
modernc.org/sqlite v1.34.5 // indirect
|
||||
modernc.org/sqlite v1.35.0 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
||||
@@ -90,10 +91,10 @@ require (
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/gorm v1.25.12
|
||||
|
||||
37
go.sum
37
go.sum
@@ -4,6 +4,8 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/celestix/gotgproto v1.0.0-beta20.1 h1:F7H08CuSiHP0YlZqATBi2wJvg7dxXFvFbpauWFd0IbI=
|
||||
github.com/celestix/gotgproto v1.0.0-beta20.1/go.mod h1:j42ZhBMUke6QyBLvCgx8tA+TL9L3+pq/Q46B+b5+3aU=
|
||||
github.com/celestix/gotgproto v1.0.0-beta20.2 h1:+WcsKdsyj4xy+TAV+4Sw6zp1xiQrIr4dMnM31+k8NYM=
|
||||
github.com/celestix/gotgproto v1.0.0-beta20.2/go.mod h1:j42ZhBMUke6QyBLvCgx8tA+TL9L3+pq/Q46B+b5+3aU=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -19,6 +21,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
|
||||
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -30,6 +34,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
@@ -79,6 +85,8 @@ github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
||||
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
||||
github.com/gotd/td v0.118.0 h1:iPGkaOAd3QO72TcvzNJGKGpLDzYOW8GIz+Va2upxBbY=
|
||||
github.com/gotd/td v0.118.0/go.mod h1:FUNVeJB9Id2Vqps9yF+8kmBNNyCGO6VXDyO8Ah7bVSw=
|
||||
github.com/gotd/td v0.120.0 h1:XeiafJM82/9SaB+ZMjMm/dnUx5+avINwVZOEsnV0zMo=
|
||||
github.com/gotd/td v0.120.0/go.mod h1:BCc2jFj1l5zP9Trk4J7nxeqW0KBGl6K95eXMgszkbOI=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
@@ -111,6 +119,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ogen-go/ogen v1.9.0 h1:n+lDQpiSFYC9G4hTvuNVWnqmIP0LR8ws0faDn9jX3hU=
|
||||
github.com/ogen-go/ogen v1.9.0/go.mod h1:vkHpuRyzjdfuRCy81EShi4t9sIgZDcNPGmiDKipRloc=
|
||||
github.com/ogen-go/ogen v1.10.0 h1:x3ukRtq/pdn/k8+pYBtqWceVASiSmgK9M5lrH89Q+04=
|
||||
github.com/ogen-go/ogen v1.10.0/go.mod h1:WExXrswerPzGWD0NpzBFsz+5eQIbP7HAtZUmpV8dqqI=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
@@ -182,42 +192,63 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -240,12 +271,16 @@ modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.23.15 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc=
|
||||
modernc.org/ccgo/v4 v4.23.15/go.mod h1:nJX30dks/IWuBOnVa7VRii9Me4/9TZ1SC9GNtmARTy0=
|
||||
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.6.2 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds=
|
||||
modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
||||
modernc.org/libc v1.61.11 h1:6sZG8uB6EMMG7iTLPTndi8jyTdgAQNIeLGjCFICACZw=
|
||||
modernc.org/libc v1.61.11/go.mod h1:HHX+srFdn839oaJRd0W8hBM3eg+mieyZCAjWwB08/nM=
|
||||
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
||||
@@ -256,6 +291,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
|
||||
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
|
||||
modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw=
|
||||
modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
@@ -20,7 +19,6 @@ import (
|
||||
type Alist struct {
|
||||
client *http.Client
|
||||
token string
|
||||
basePath string
|
||||
baseURL string
|
||||
loginInfo *loginRequest
|
||||
}
|
||||
@@ -42,6 +40,15 @@ type loginResponse struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type meResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type putResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
@@ -105,7 +112,6 @@ func (a *Alist) refreshToken() {
|
||||
}
|
||||
|
||||
func (a *Alist) Init() {
|
||||
a.basePath = config.Cfg.Storage.Alist.BasePath
|
||||
a.baseURL = config.Cfg.Storage.Alist.URL
|
||||
a.client = &http.Client{
|
||||
Timeout: 12 * time.Hour,
|
||||
@@ -113,6 +119,44 @@ func (a *Alist) Init() {
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
if config.Cfg.Storage.Alist.Token != "" {
|
||||
a.token = config.Cfg.Storage.Alist.Token
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"/api/me", nil)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to create request: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
req.Header.Set("Authorization", a.token)
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to send request: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.L.Fatalf("Failed to get alist user info: %s", resp.Status)
|
||||
os.Exit(1)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to read response body: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var meResp meResponse
|
||||
if err := json.Unmarshal(body, &meResp); err != nil {
|
||||
logger.L.Fatalf("Failed to unmarshal me response: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if meResp.Code != http.StatusOK {
|
||||
logger.L.Fatalf("Failed to get alist user info: %s", meResp.Message)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.L.Debugf("Logged in Alist as %s", meResp.Data.Username)
|
||||
return
|
||||
}
|
||||
a.loginInfo = &loginRequest{
|
||||
Username: config.Cfg.Storage.Alist.Username,
|
||||
Password: config.Cfg.Storage.Alist.Password,
|
||||
@@ -128,7 +172,6 @@ func (a *Alist) Init() {
|
||||
}
|
||||
|
||||
func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
storagePath = path.Join(a.basePath, storagePath)
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
|
||||
@@ -21,5 +21,12 @@ func (l *Local) Init() {
|
||||
}
|
||||
|
||||
func (l *Local) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
return fileutil.CopyFile(filePath, filepath.Join(config.Cfg.Storage.Local.BasePath, storagePath))
|
||||
absPath, err := filepath.Abs(storagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileutil.CreateDir(filepath.Dir(absPath)); err != nil {
|
||||
return err
|
||||
}
|
||||
return fileutil.CopyFile(filePath, storagePath)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package storage
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
@@ -16,7 +18,7 @@ import (
|
||||
|
||||
type Storage interface {
|
||||
Init()
|
||||
Save(cttx context.Context, filePath, storagePath string) error
|
||||
Save(cttx context.Context, localFilePath, storagePath string) error
|
||||
}
|
||||
|
||||
var Storages = make(map[types.StorageType]Storage)
|
||||
@@ -47,6 +49,7 @@ func Init() {
|
||||
}
|
||||
|
||||
func Save(storageType types.StorageType, ctx context.Context, filePath, storagePath string) error {
|
||||
logger.L.Debugf("Saving file %s to storage: [%s] %s", filePath, storageType, storagePath)
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
@@ -59,7 +62,16 @@ func Save(storageType types.StorageType, ctx context.Context, filePath, storageP
|
||||
wg.Add(1)
|
||||
go func(storage Storage) {
|
||||
defer wg.Done()
|
||||
if err := storage.Save(ctx, filePath, storagePath); err != nil {
|
||||
storageDestPath := storagePath
|
||||
switch storage.(type) {
|
||||
case *local.Local:
|
||||
storageDestPath = filepath.Join(config.Cfg.Storage.Local.BasePath, storagePath)
|
||||
case *webdav.Webdav:
|
||||
storageDestPath = path.Join(config.Cfg.Storage.Webdav.BasePath, storagePath)
|
||||
case *alist.Alist:
|
||||
storageDestPath = path.Join(config.Cfg.Storage.Alist.BasePath, storagePath)
|
||||
}
|
||||
if err := storage.Save(ctx, filePath, storageDestPath); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}(storage)
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
@@ -16,13 +14,11 @@ import (
|
||||
type Webdav struct{}
|
||||
|
||||
var (
|
||||
Client *gowebdav.Client
|
||||
basePath string
|
||||
Client *gowebdav.Client
|
||||
)
|
||||
|
||||
func (w *Webdav) Init() {
|
||||
webdavConfig := config.Cfg.Storage.Webdav
|
||||
basePath = strings.TrimSuffix(webdavConfig.BasePath, "/")
|
||||
Client = gowebdav.NewClient(webdavConfig.URL, webdavConfig.Username, webdavConfig.Password)
|
||||
if err := Client.Connect(); err != nil {
|
||||
logger.L.Fatalf("Failed to connect to webdav server: %v", err)
|
||||
@@ -32,9 +28,8 @@ func (w *Webdav) Init() {
|
||||
}
|
||||
|
||||
func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
storagePath = path.Join(basePath, storagePath)
|
||||
if err := Client.MkdirAll(filepath.Dir(storagePath), os.ModePerm); err != nil {
|
||||
logger.L.Errorf("Failed to create directory %s: %v", filepath.Dir(storagePath), err)
|
||||
if err := Client.MkdirAll(path.Dir(storagePath), os.ModePerm); err != nil {
|
||||
logger.L.Errorf("Failed to create directory %s: %v", path.Dir(storagePath), err)
|
||||
return ErrFailedToCreateDirectory
|
||||
}
|
||||
file, err := os.Open(filePath)
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
type ReceivedFile struct {
|
||||
gorm.Model
|
||||
Processing bool
|
||||
ChatID int64
|
||||
MessageID int
|
||||
ChatID int64 `gorm:"uniqueIndex:idx_chat_id_message_id;not null"`
|
||||
MessageID int `gorm:"uniqueIndex:idx_chat_id_message_id;not null"`
|
||||
ReplyMessageID int
|
||||
ReplyChatID int64
|
||||
FileName string
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
)
|
||||
@@ -34,14 +37,16 @@ type Task struct {
|
||||
File *File
|
||||
Storage StorageType
|
||||
StoragePath string
|
||||
StartTime time.Time
|
||||
|
||||
MessageID int
|
||||
ChatID int64
|
||||
FileMessageID int
|
||||
FileChatID int64
|
||||
ReplyMessageID int
|
||||
ReplyChatID int64
|
||||
}
|
||||
|
||||
func (t Task) String() string {
|
||||
return fmt.Sprintf("[%d:%d]:%s", t.ChatID, t.MessageID, t.File.FileName)
|
||||
return fmt.Sprintf("[%d:%d]:%s", t.FileChatID, t.FileMessageID, t.File.FileName)
|
||||
}
|
||||
|
||||
func (t Task) FileName() string {
|
||||
@@ -53,3 +58,18 @@ type File struct {
|
||||
FileSize int64
|
||||
FileName string
|
||||
}
|
||||
|
||||
func (f File) Hash() string {
|
||||
locationBytes := []byte(f.Location.String())
|
||||
fileSizeBytes := []byte(fmt.Sprintf("%d", f.FileSize))
|
||||
fileNameBytes := []byte(f.FileName)
|
||||
|
||||
structBytes := append(locationBytes, fileSizeBytes...)
|
||||
structBytes = append(structBytes, fileNameBytes...)
|
||||
|
||||
hash := md5.New()
|
||||
hash.Write(structBytes)
|
||||
hashBytes := hash.Sum(nil)
|
||||
|
||||
return hex.EncodeToString(hashBytes)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user