Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2579044841 | ||
|
|
88a02aae8d | ||
|
|
ab374a870b | ||
|
|
3a1b8f34ea | ||
|
|
c4eb824457 | ||
|
|
692e970772 | ||
|
|
80696c9661 | ||
|
|
18cd480264 | ||
|
|
dfde65c28e | ||
|
|
968547b005 | ||
|
|
9367419156 | ||
|
|
f80c4d7d55 | ||
|
|
ccfde34666 | ||
|
|
2b23446123 | ||
|
|
7882185ee1 | ||
|
|
2d17a731c4 | ||
|
|
db69688722 | ||
|
|
ec09289d5f | ||
|
|
13c87debcc | ||
|
|
5f3b38c788 | ||
|
|
8ba0c623c9 | ||
|
|
6fa8e89191 | ||
|
|
3a4effab33 | ||
|
|
7692286d78 | ||
|
|
93ffc940ce | ||
|
|
4aadfc1273 | ||
|
|
d26a8df15f | ||
|
|
a746cc0fc7 | ||
|
|
0fb5634874 | ||
|
|
930e838b2e | ||
|
|
1701d1ab86 | ||
|
|
a17492d4ae | ||
|
|
a32bf43cdc | ||
|
|
a25a58f8a2 |
36
README.md
36
README.md
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
# <img src="docs/logo.jpg" width="45" align="center"> Save Any Bot
|
# <img src="docs/logo.jpg" width="45" align="center"> Save Any Bot
|
||||||
|
|
||||||
|
**简体中文** | [English](README_EN.md)
|
||||||
|
|
||||||
把 Telegram 的文件保存到各类存储端.
|
把 Telegram 的文件保存到各类存储端.
|
||||||
|
|
||||||
> _就像 PikPak Bot 一样_
|
> _就像 PikPak Bot 一样_
|
||||||
@@ -18,6 +20,8 @@ Demo Video:
|
|||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
|
### 从二进制文件部署
|
||||||
|
|
||||||
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
|
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
|
||||||
|
|
||||||
在解压后目录新建 `config.toml` 文件, 参考 [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
在解压后目录新建 `config.toml` 文件, 参考 [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
||||||
@@ -29,7 +33,7 @@ chmod +x saveany-bot
|
|||||||
./saveany-bot
|
./saveany-bot
|
||||||
```
|
```
|
||||||
|
|
||||||
### 添加为 systemd 服务
|
#### 添加为 systemd 服务
|
||||||
|
|
||||||
创建文件 `/etc/systemd/system/saveany-bot.service` 并写入以下内容:
|
创建文件 `/etc/systemd/system/saveany-bot.service` 并写入以下内容:
|
||||||
|
|
||||||
@@ -54,6 +58,27 @@ WantedBy=multi-user.target
|
|||||||
systemctl enable --now saveany-bot
|
systemctl enable --now saveany-bot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 使用 Docker 部署
|
||||||
|
|
||||||
|
#### Docker Compose
|
||||||
|
|
||||||
|
下载 [docker-compose.yml](https://github.com/krau/SaveAny-Bot/blob/main/docker-compose.yml) 文件, 在同目录下新建 `config.toml` 文件, 参考 [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
||||||
|
|
||||||
|
启动:
|
||||||
|
|
||||||
|
```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` 升级到最新版
|
使用 `upgrade` 或 `up` 升级到最新版
|
||||||
@@ -62,9 +87,16 @@ systemctl enable --now saveany-bot
|
|||||||
./saveany-bot upgrade
|
./saveany-bot upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如果是 Docker 部署, 使用以下命令更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull ghcr.io/krau/saveany-bot:latest
|
||||||
|
docker restart saveany-bot
|
||||||
|
```
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
向 Bot 发送(转发)文件, 按照提示操作.
|
向 Bot 发送(转发)文件, 或发送公开频道的消息链接, 按照提示操作.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
108
README_EN.md
Normal file
108
README_EN.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# <img src="docs/logo.jpg" width="45" align="center"> Save Any Bot
|
||||||
|
|
||||||
|
[简体中文](README.md) | **English**
|
||||||
|
|
||||||
|
Save Telegram files to various storage endpoints.
|
||||||
|
|
||||||
|
> _Just like PikPak Bot_
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Demo Video:
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[SaveAny-Bot Demo Video.webm](https://github.com/user-attachments/assets/a0de2453-a4d1-4a12-81fb-9d84856dce09)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Deploy from Binary
|
||||||
|
|
||||||
|
Download the binary file for your platform from the [Release](https://github.com/krau/SaveAny-Bot/releases) page.
|
||||||
|
|
||||||
|
Create a `config.toml` file in the extracted directory, refer to [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) for configuration.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x saveany-bot
|
||||||
|
./saveany-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add as systemd Service
|
||||||
|
|
||||||
|
Create file `/etc/systemd/system/saveany-bot.service` and write the following content:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=SaveAnyBot
|
||||||
|
After=systemd-user-sessions.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/yourpath/
|
||||||
|
ExecStart=/yourpath/saveany-bot
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable auto-start and start the service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl enable --now saveany-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy with Docker
|
||||||
|
|
||||||
|
#### Docker Compose
|
||||||
|
|
||||||
|
Download [docker-compose.yml](https://github.com/krau/SaveAny-Bot/blob/main/docker-compose.yml) file and create a `config.toml` file in the same directory, refer to [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) for configuration.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update
|
||||||
|
|
||||||
|
Use `upgrade` or `up` command to upgrade to the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./saveany-bot upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
If deployed with Docker, use the following commands to update:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull ghcr.io/krau/saveany-bot:latest
|
||||||
|
docker restart saveany-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Send (forward) files to the Bot and follow the prompts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
- [gotd](https://github.com/gotd/td)
|
||||||
|
- [TG-FileStreamBot](https://github.com/EverythingSuckz/TG-FileStreamBot)
|
||||||
|
- [gotgproto](https://github.com/celestix/gotgproto)
|
||||||
|
- All the dependencies
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/bot"
|
"github.com/krau/SaveAny-Bot/bot"
|
||||||
"github.com/krau/SaveAny-Bot/common"
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
@@ -10,12 +13,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func InitAll() {
|
func InitAll() {
|
||||||
config.Init()
|
if err := config.Init(); err != nil {
|
||||||
|
fmt.Println("加载配置文件失败: ", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
logger.InitLogger()
|
logger.InitLogger()
|
||||||
logger.L.Info("Running...")
|
logger.L.Info("正在启动 SaveAny-Bot...")
|
||||||
|
storage.LoadStorages()
|
||||||
common.Init()
|
common.Init()
|
||||||
storage.Init()
|
|
||||||
dao.Init()
|
dao.Init()
|
||||||
bot.Init()
|
bot.Init()
|
||||||
}
|
}
|
||||||
|
|||||||
26
bot/bot.go
26
bot/bot.go
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/celestix/gotgproto/sessionMaker"
|
"github.com/celestix/gotgproto/sessionMaker"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/gotd/td/telegram/dcs"
|
"github.com/gotd/td/telegram/dcs"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
@@ -26,7 +27,7 @@ func newProxyDialer(proxyUrl string) (proxy.Dialer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
logger.L.Info("Initializing client...")
|
logger.L.Info("初始化 Telegram 客户端...")
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
resultChan := make(chan struct {
|
resultChan := make(chan struct {
|
||||||
@@ -60,6 +61,23 @@ func Init() {
|
|||||||
Resolver: resolver,
|
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: "保存所回复的文件"},
|
||||||
|
},
|
||||||
|
})
|
||||||
resultChan <- struct {
|
resultChan <- struct {
|
||||||
client *gotgproto.Client
|
client *gotgproto.Client
|
||||||
err error
|
err error
|
||||||
@@ -68,15 +86,15 @@ func Init() {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
logger.L.Fatal("Failed to initialize client: timeout")
|
logger.L.Fatal("初始化客户端失败: 超时")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
if result.err != nil {
|
if result.err != nil {
|
||||||
logger.L.Fatalf("Failed to initialize client: %s", result.err)
|
logger.L.Fatalf("初始化客户端失败: %s", result.err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
Client = result.client
|
Client = result.client
|
||||||
RegisterHandlers(Client.Dispatcher)
|
RegisterHandlers(Client.Dispatcher)
|
||||||
logger.L.Info("Client initialized")
|
logger.L.Info("客户端初始化完成")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
110
bot/handle_link.go
Normal file
110
bot/handle_link.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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/storage"
|
||||||
|
"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("解析消息 ID 失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无法解析消息 ID"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
chatUsername := strSlice[1]
|
||||||
|
linkChat, err := ctx.ResolveUsername(chatUsername)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("解析 Chat ID 失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无法解析 Chat ID"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if linkChat == nil {
|
||||||
|
logger.L.Errorf("无法找到聊天: %s", chatUsername)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无法找到聊天"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("获取用户失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
storages := storage.GetUserStorages(user.ChatID)
|
||||||
|
|
||||||
|
if len(storages) == 0 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件..."), nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("回复失败: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := FileFromMessage(ctx, linkChat.GetID(), messageID, "")
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("获取文件失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取文件失败: "+err.Error()), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
// TODO: Better file name
|
||||||
|
if file.FileName == "" {
|
||||||
|
logger.L.Warnf("文件名为空,使用生成的名称")
|
||||||
|
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("保存接收的文件失败: %s", err)
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: "无法保存文件: " + err.Error(),
|
||||||
|
ID: replied.ID,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if !user.Silent || user.DefaultStorage == "" {
|
||||||
|
return ProvideSelectMessage(ctx, update, file, linkChat.GetID(), messageID, replied.ID)
|
||||||
|
}
|
||||||
|
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||||
|
Ctx: ctx,
|
||||||
|
Status: types.Pending,
|
||||||
|
File: file,
|
||||||
|
StorageName: user.DefaultStorage,
|
||||||
|
FileChatID: linkChat.GetID(),
|
||||||
|
FileMessageID: messageID,
|
||||||
|
ReplyMessageID: replied.ID,
|
||||||
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
|
})
|
||||||
|
}
|
||||||
75
bot/handler_conversation.go
Normal file
75
bot/handler_conversation.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConversationType string
|
||||||
|
|
||||||
|
type ConversationState struct {
|
||||||
|
sync.Mutex
|
||||||
|
conversationType ConversationType
|
||||||
|
InConversation bool
|
||||||
|
data map[ConversationType]map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConversationState) Reset() {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
c.InConversation = false
|
||||||
|
c.conversationType = ""
|
||||||
|
c.data = make(map[ConversationType]map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConversationState) SetConversationType(t ConversationType) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
c.conversationType = t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConversationState) GetData(key string) interface{} {
|
||||||
|
if c.data == nil || c.data[c.conversationType] == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.data[c.conversationType][key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConversationState) SetData(key string, value interface{}) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
if c.data == nil {
|
||||||
|
c.data = make(map[ConversationType]map[string]interface{})
|
||||||
|
}
|
||||||
|
if c.data[c.conversationType] == nil {
|
||||||
|
c.data[c.conversationType] = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
c.data[c.conversationType][key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement conversation handling
|
||||||
|
// var userConversationState = make(map[int64]*ConversationState)
|
||||||
|
|
||||||
|
// func handleConversation(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
// userID := update.EffectiveUser().GetID()
|
||||||
|
// state, ok := userConversationState[userID]
|
||||||
|
// if !ok {
|
||||||
|
// return dispatcher.ContinueGroups
|
||||||
|
// }
|
||||||
|
// if update.EffectiveMessage.Text == "/cancel" {
|
||||||
|
// state.Reset()
|
||||||
|
// ctx.Reply(update, ext.ReplyTextString("已取消"), nil)
|
||||||
|
// return dispatcher.EndGroups
|
||||||
|
// }
|
||||||
|
// if !state.InConversation {
|
||||||
|
// return dispatcher.ContinueGroups
|
||||||
|
// }
|
||||||
|
// return handleConversationState(ctx, update, state)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func handleConversationState(ctx *ext.Context, update *ext.Update, state *ConversationState) error {
|
||||||
|
// switch state.conversationType {
|
||||||
|
// default:
|
||||||
|
// logger.L.Errorf("Unknown conversation type: %s", state.conversationType)
|
||||||
|
// }
|
||||||
|
// return dispatcher.EndGroups
|
||||||
|
// }
|
||||||
376
bot/handlers.go
376
bot/handlers.go
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"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/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
|
||||||
@@ -27,20 +27,26 @@ func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
|
|||||||
dispatcher.AddHandler(handlers.NewCommand("start", start))
|
dispatcher.AddHandler(handlers.NewCommand("start", start))
|
||||||
dispatcher.AddHandler(handlers.NewCommand("help", help))
|
dispatcher.AddHandler(handlers.NewCommand("help", help))
|
||||||
dispatcher.AddHandler(handlers.NewCommand("silent", silent))
|
dispatcher.AddHandler(handlers.NewCommand("silent", silent))
|
||||||
dispatcher.AddHandler(handlers.NewCommand("storage", setDefaultStorage))
|
dispatcher.AddHandler(handlers.NewCommand("storage", storageCmd))
|
||||||
dispatcher.AddHandler(handlers.NewCommand("save", saveCmd))
|
dispatcher.AddHandler(handlers.NewCommand("save", saveCmd))
|
||||||
|
linkRegexFilter, err := filters.Message.Regex(linkRegexString)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Panicf("创建正则表达式过滤器失败: %s", err)
|
||||||
|
}
|
||||||
|
dispatcher.AddHandler(handlers.NewMessage(linkRegexFilter, handleLinkMessage))
|
||||||
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("add"), AddToQueue))
|
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("add"), AddToQueue))
|
||||||
|
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("set_default"), setDefaultStorage))
|
||||||
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
|
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
|
||||||
}
|
}
|
||||||
|
|
||||||
const noPermissionText string = `
|
const noPermissionText string = `
|
||||||
本 Bot 仅限个人使用.
|
您不在白名单中, 无法使用此 Bot.
|
||||||
您可以部署自己的实例: https://github.com/krau/SaveAny-Bot
|
您可以部署自己的实例: https://github.com/krau/SaveAny-Bot
|
||||||
`
|
`
|
||||||
|
|
||||||
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
||||||
userID := update.GetUserChat().GetID()
|
userID := update.GetUserChat().GetID()
|
||||||
if !slice.Contain(config.Cfg.Telegram.Admins, userID) {
|
if !slice.Contain(config.Cfg.GetUsersID(), userID) {
|
||||||
ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil)
|
ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -49,22 +55,26 @@ func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
|||||||
|
|
||||||
func start(ctx *ext.Context, update *ext.Update) error {
|
func start(ctx *ext.Context, update *ext.Update) error {
|
||||||
if err := dao.CreateUser(update.GetUserChat().GetID()); err != nil {
|
if err := dao.CreateUser(update.GetUserChat().GetID()); err != nil {
|
||||||
logger.L.Errorf("Failed to create user: %s", err)
|
logger.L.Errorf("创建用户失败: %s", err)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
return help(ctx, update)
|
return help(ctx, update)
|
||||||
}
|
}
|
||||||
|
|
||||||
const helpText string = `
|
const helpText string = `
|
||||||
SaveAny Bot - 转存你的 Telegram 文件
|
Save Any Bot - 转存你的 Telegram 文件
|
||||||
命令:
|
命令:
|
||||||
/start - 开始使用
|
/start - 开始使用
|
||||||
/help - 显示帮助
|
/help - 显示帮助
|
||||||
/silent - 静默模式
|
/silent - 开关静默模式
|
||||||
/storage - 设置默认存储位置
|
/storage - 设置默认存储位置
|
||||||
/save - 保存文件
|
/save [自定义文件名] - 保存文件
|
||||||
|
|
||||||
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
|
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
|
||||||
|
|
||||||
|
默认存储位置: 在静默模式下保存到的位置
|
||||||
|
|
||||||
|
向 Bot 发送(转发)文件, 或发送一个公开频道的消息链接以保存文件
|
||||||
`
|
`
|
||||||
|
|
||||||
func help(ctx *ext.Context, update *ext.Update) error {
|
func help(ctx *ext.Context, update *ext.Update) error {
|
||||||
@@ -73,60 +83,18 @@ func help(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func silent(ctx *ext.Context, update *ext.Update) error {
|
func silent(ctx *ext.Context, update *ext.Update) error {
|
||||||
user, err := dao.GetUserByUserID(update.GetUserChat().GetID())
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to get user: %s", err)
|
logger.L.Errorf("获取用户失败: %s", err)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
user.Silent = !user.Silent
|
user.Silent = !user.Silent
|
||||||
if err := dao.UpdateUser(user); err != nil {
|
if err := dao.UpdateUser(user); err != nil {
|
||||||
logger.L.Errorf("Failed to update user: %s", err)
|
logger.L.Errorf("更新用户失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("更新用户失败"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已%s静默模式", func() string {
|
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已%s静默模式", map[bool]string{true: "开启", false: "关闭"}[user.Silent])), nil)
|
||||||
if user.Silent {
|
|
||||||
return "开启"
|
|
||||||
}
|
|
||||||
return "关闭"
|
|
||||||
}())), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDefaultStorage(ctx *ext.Context, update *ext.Update) error {
|
|
||||||
if len(storage.Storages) == 0 {
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("未配置存储"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
args := strings.Split(update.EffectiveMessage.Text, " ")
|
|
||||||
avaliableStorages := maputil.Keys(storage.Storages)
|
|
||||||
if len(args) < 2 {
|
|
||||||
text := []styling.StyledTextOption{
|
|
||||||
styling.Plain("请提供存储位置名称, 可用项:"),
|
|
||||||
}
|
|
||||||
for _, name := range avaliableStorages {
|
|
||||||
text = append(text, styling.Plain("\n"))
|
|
||||||
text = append(text, styling.Code(name))
|
|
||||||
}
|
|
||||||
text = append(text, styling.Plain("\n示例: /storage local"))
|
|
||||||
ctx.Reply(update, ext.ReplyTextStyledTextArray(text), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
storageName := args[1]
|
|
||||||
if !slice.Contain(avaliableStorages, storageName) {
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("存储位置不存在"), 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
|
|
||||||
}
|
|
||||||
user.DefaultStorage = storageName
|
|
||||||
if err := dao.UpdateUser(user); err != nil {
|
|
||||||
logger.L.Errorf("Failed to update user: %s", err)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已设置默认存储位置为 %s", storageName)), nil)
|
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +114,27 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
msg, err := GetTGMessage(ctx, Client, replyToMsgID)
|
|
||||||
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("获取用户失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
storages := storage.GetUserStorages(user.ChatID)
|
||||||
|
|
||||||
|
if len(storages) == 0 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := GetTGMessage(ctx, update.EffectiveChat().GetID(), replyToMsgID)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("获取消息失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无法获取消息"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
supported, _ := supportedMediaFilter(msg)
|
supported, _ := supportedMediaFilter(msg)
|
||||||
if !supported {
|
if !supported {
|
||||||
@@ -154,85 +142,136 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return dispatcher.EndGroups
|
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)
|
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to reply: %s", err)
|
logger.L.Errorf("回复失败: %s", err)
|
||||||
return dispatcher.EndGroups
|
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 {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to get file from message: %s", err)
|
logger.L.Errorf("获取文件失败: %s", err)
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: "无法获取文件",
|
Message: fmt.Sprintf("获取文件失败: %s", err),
|
||||||
ID: replied.ID,
|
ID: replied.ID,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: better file name
|
||||||
if file.FileName == "" {
|
if file.FileName == "" {
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
file.FileName = fmt.Sprintf("%d_%d_%s", update.EffectiveChat().GetID(), replyToMsgID, file.Hash())
|
||||||
Message: "无法获取文件名",
|
|
||||||
ID: replied.ID,
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
}
|
||||||
|
receivedFile := &types.ReceivedFile{
|
||||||
if err := dao.AddReceivedFile(&types.ReceivedFile{
|
|
||||||
Processing: false,
|
Processing: false,
|
||||||
FileName: file.FileName,
|
FileName: file.FileName,
|
||||||
ChatID: update.EffectiveChat().GetID(),
|
ChatID: update.EffectiveChat().GetID(),
|
||||||
MessageID: replyToMsgID,
|
MessageID: replyToMsgID,
|
||||||
ReplyMessageID: replied.ID,
|
ReplyMessageID: replied.ID,
|
||||||
}); err != nil {
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
logger.L.Errorf("Failed to add received file: %s", err)
|
}
|
||||||
|
|
||||||
|
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
||||||
|
logger.L.Errorf("保存接收的文件失败: %s", err)
|
||||||
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: "无法保存文件",
|
Message: fmt.Sprintf("保存接收的文件失败: %s", err),
|
||||||
ID: replied.ID,
|
ID: replied.ID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.L.Errorf("Failed to edit message: %s", err)
|
logger.L.Errorf("编辑消息失败: %s", err)
|
||||||
}
|
}
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
if !user.Silent || user.DefaultStorage == "" {
|
||||||
if !user.Silent {
|
return ProvideSelectMessage(ctx, update, file, update.EffectiveChat().GetID(), msg.ID, replied.ID)
|
||||||
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 HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||||
if user.DefaultStorage == "" {
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("请先使用 /storage 设置默认存储位置"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
queue.AddTask(types.Task{
|
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Status: types.Pending,
|
Status: types.Pending,
|
||||||
File: file,
|
File: file,
|
||||||
Storage: types.StorageType(user.DefaultStorage),
|
StorageName: user.DefaultStorage,
|
||||||
ChatID: update.EffectiveChat().GetID(),
|
FileChatID: update.EffectiveChat().GetID(),
|
||||||
ReplyMessageID: replied.ID,
|
ReplyMessageID: replied.ID,
|
||||||
MessageID: msg.ID,
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
})
|
FileMessageID: msg.ID,
|
||||||
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
UserID: user.ChatID,
|
||||||
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", file.FileName, queue.Len()),
|
|
||||||
ID: replied.ID,
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func storageCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to edit message: %s", err)
|
logger.L.Errorf("获取用户失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
storages := storage.GetUserStorages(user.ChatID)
|
||||||
|
if len(storages) == 0 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("请选择要设为默认的存储位置"), &ext.ReplyOpts{
|
||||||
|
Markup: getSetDefaultStorageMarkup(user.ChatID, storages),
|
||||||
|
})
|
||||||
|
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDefaultStorage(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
args := strings.Split(string(update.CallbackQuery.Data), " ")
|
||||||
|
userID, _ := strconv.Atoi(args[1])
|
||||||
|
storageNameHash := args[2]
|
||||||
|
if userID != int(update.CallbackQuery.GetUserID()) {
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "你没有权限",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
storageName := storageHashName[storageNameHash]
|
||||||
|
selectedStorage, err := storage.GetStorageByName(storageName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("获取指定存储失败: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "获取指定存储失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
user, err := dao.GetUserByChatID(int64(userID))
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to get user: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "获取用户失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
user.DefaultStorage = storageName
|
||||||
|
if err := dao.UpdateUser(user); err != nil {
|
||||||
|
logger.L.Errorf("Failed to update user: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "更新用户失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("已将 %s (%s) 设为默认存储位置", selectedStorage.Name(), selectedStorage.Type()),
|
||||||
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,87 +285,70 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := dao.GetUserByUserID(update.GetUserChat().GetID())
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to get user: %s", err)
|
logger.L.Errorf("获取用户失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
storages := storage.GetUserStorages(user.ChatID)
|
||||||
|
if len(storages) == 0 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to reply: %s", err)
|
logger.L.Errorf("回复失败: %s", err)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
media := update.EffectiveMessage.Media
|
media := update.EffectiveMessage.Media
|
||||||
file, err := FileFromMedia(media)
|
file, err := FileFromMedia(media, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to get file from media: %s", err)
|
logger.L.Errorf("获取文件失败: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("无法获取文件"), nil)
|
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if file.FileName == "" {
|
if file.FileName == "" {
|
||||||
ctx.Reply(update, ext.ReplyTextString("无法获取文件名"), nil)
|
file.FileName = fmt.Sprintf("%d_%d_%s", update.EffectiveChat().GetID(), update.EffectiveMessage.ID, file.Hash())
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dao.AddReceivedFile(&types.ReceivedFile{
|
if err := dao.SaveReceivedFile(&types.ReceivedFile{
|
||||||
Processing: false,
|
Processing: false,
|
||||||
FileName: file.FileName,
|
FileName: file.FileName,
|
||||||
ChatID: update.EffectiveChat().GetID(),
|
ChatID: update.EffectiveChat().GetID(),
|
||||||
MessageID: update.EffectiveMessage.ID,
|
MessageID: update.EffectiveMessage.ID,
|
||||||
ReplyMessageID: msg.ID,
|
ReplyMessageID: msg.ID,
|
||||||
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.L.Errorf("Failed to add received file: %s", err)
|
logger.L.Errorf("添加接收的文件失败: %s", err)
|
||||||
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: "无法保存文件",
|
Message: fmt.Sprintf("添加接收的文件失败: %s", err),
|
||||||
ID: msg.ID,
|
ID: msg.ID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.L.Errorf("Failed to edit message: %s", err)
|
logger.L.Errorf("编辑消息失败: %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 dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.DefaultStorage == "" {
|
if !user.Silent || user.DefaultStorage == "" {
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
return ProvideSelectMessage(ctx, update, file, update.EffectiveChat().GetID(), update.EffectiveMessage.ID, msg.ID)
|
||||||
Message: "请先使用 /storage 设置默认存储位置",
|
|
||||||
ID: msg.ID,
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
}
|
||||||
|
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||||
queue.AddTask(types.Task{
|
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Status: types.Pending,
|
Status: types.Pending,
|
||||||
File: file,
|
File: file,
|
||||||
Storage: types.StorageType(user.DefaultStorage),
|
StorageName: user.DefaultStorage,
|
||||||
ChatID: update.EffectiveChat().GetID(),
|
FileChatID: update.EffectiveChat().GetID(),
|
||||||
ReplyMessageID: msg.ID,
|
ReplyMessageID: msg.ID,
|
||||||
MessageID: update.EffectiveMessage.ID,
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
|
FileMessageID: update.EffectiveMessage.ID,
|
||||||
|
UserID: user.ChatID,
|
||||||
})
|
})
|
||||||
|
|
||||||
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 {
|
func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
||||||
if !slice.Contain(config.Cfg.Telegram.Admins, update.CallbackQuery.UserID) {
|
if !slice.Contain(config.Cfg.GetUsersID(), update.CallbackQuery.UserID) {
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
QueryID: update.CallbackQuery.QueryID,
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
Alert: true,
|
Alert: true,
|
||||||
@@ -336,11 +358,24 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
args := strings.Split(string(update.CallbackQuery.Data), " ")
|
args := strings.Split(string(update.CallbackQuery.Data), " ")
|
||||||
messageID, _ := strconv.Atoi(args[1])
|
fileChatID, _ := strconv.Atoi(args[1])
|
||||||
logger.L.Tracef("Got add to queue: chatID: %d, messageID: %d, storage: %s", update.EffectiveChat().GetID(), messageID, args[2])
|
fileMessageID, _ := strconv.Atoi(args[2])
|
||||||
record, err := dao.GetReceivedFileByChatAndMessageID(update.EffectiveChat().GetID(), messageID)
|
storageNameHash := args[3]
|
||||||
|
storageName := storageHashName[storageNameHash]
|
||||||
|
if storageName == "" {
|
||||||
|
logger.L.Errorf("未知存储位置哈希: %d", storageNameHash)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "未知存储位置",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
logger.L.Tracef("Got add to queue: chatID: %d, messageID: %d, storage: %s", fileChatID, fileMessageID, storageName)
|
||||||
|
record, err := dao.GetReceivedFileByChatAndMessageID(int64(fileChatID), fileMessageID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to get received file: %s", err)
|
logger.L.Errorf("获取记录失败: %s", err)
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
QueryID: update.CallbackQuery.QueryID,
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
Alert: true,
|
Alert: true,
|
||||||
@@ -351,18 +386,17 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
if update.CallbackQuery.MsgID != record.ReplyMessageID {
|
if update.CallbackQuery.MsgID != record.ReplyMessageID {
|
||||||
record.ReplyMessageID = update.CallbackQuery.MsgID
|
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)
|
logger.L.Errorf("更新接收的文件失败: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file, err := FileFromMessage(ctx, record.ChatID, record.MessageID, record.FileName)
|
||||||
file, err := FileFromMessage(ctx, Client, record.ChatID, record.MessageID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to get file from message: %s", err)
|
logger.L.Errorf("获取消息中的文件失败: %s", err)
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
QueryID: update.CallbackQuery.QueryID,
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
Alert: true,
|
Alert: true,
|
||||||
Message: "获取消息文件失败",
|
Message: fmt.Sprintf("获取消息中的文件失败: %s", err),
|
||||||
CacheTime: 5,
|
CacheTime: 5,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
@@ -372,14 +406,32 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
|||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Status: types.Pending,
|
Status: types.Pending,
|
||||||
File: file,
|
File: file,
|
||||||
Storage: types.StorageType(args[2]),
|
StorageName: storageName,
|
||||||
ChatID: record.ChatID,
|
FileChatID: record.ChatID,
|
||||||
ReplyMessageID: record.ReplyMessageID,
|
ReplyMessageID: record.ReplyMessageID,
|
||||||
MessageID: record.MessageID,
|
FileMessageID: record.MessageID,
|
||||||
|
ReplyChatID: record.ReplyChatID,
|
||||||
|
UserID: update.EffectiveUser().GetID(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", record.FileName, queue.Len()),
|
Message: text,
|
||||||
ID: record.ReplyMessageID,
|
Entities: entities,
|
||||||
|
ID: record.ReplyMessageID,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
212
bot/utils.go
212
bot/utils.go
@@ -1,20 +1,31 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"errors"
|
||||||
"crypto/md5"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto"
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"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/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/common"
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
|
"github.com/krau/SaveAny-Bot/queue"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"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")
|
||||||
|
ErrNoStorages = errors.New("no available storage")
|
||||||
|
)
|
||||||
|
|
||||||
func supportedMediaFilter(m *tg.Message) (bool, error) {
|
func supportedMediaFilter(m *tg.Message) (bool, error) {
|
||||||
if not := m.Media == nil; not {
|
if not := m.Media == nil; not {
|
||||||
return false, dispatcher.EndGroups
|
return false, dispatcher.EndGroups
|
||||||
@@ -29,69 +40,75 @@ func supportedMediaFilter(m *tg.Message) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var StorageDisplayNames = map[string]string{
|
// for callback data
|
||||||
"all": "全部",
|
var storageHashName = map[string]string{}
|
||||||
"local": "服务器磁盘",
|
|
||||||
"alist": "Alist",
|
|
||||||
"webdav": "WebDAV",
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAddTaskMarkup(messageID int) *tg.ReplyInlineMarkup {
|
func getSelectStorageMarkup(userChatID int64, fileChatID, fileMessageID int) (*tg.ReplyInlineMarkup, error) {
|
||||||
storageButtons := make([]tg.KeyboardButtonClass, 0)
|
user, err := dao.GetUserByChatID(userChatID)
|
||||||
for _, name := range storage.StorageKeys {
|
if err != nil {
|
||||||
storageButtons = append(storageButtons, &tg.KeyboardButtonCallback{
|
return nil, err
|
||||||
Text: StorageDisplayNames[string(name)],
|
}
|
||||||
Data: []byte(fmt.Sprintf("add %d %s", messageID, name)),
|
storages := storage.GetUserStorages(user.ChatID)
|
||||||
|
|
||||||
|
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||||
|
for _, storage := range storages {
|
||||||
|
nameHash := common.HashString(storage.Name())
|
||||||
|
storageHashName[nameHash] = storage.Name()
|
||||||
|
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
||||||
|
Text: storage.Name(),
|
||||||
|
Data: []byte(fmt.Sprintf("add %d %d %s", fileChatID, fileMessageID, nameHash)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
markup := &tg.ReplyInlineMarkup{}
|
||||||
if len(storageButtons) < 1 {
|
for i := 0; i < len(buttons); i += 3 {
|
||||||
return nil
|
row := tg.KeyboardButtonRow{}
|
||||||
}
|
row.Buttons = buttons[i:min(i+3, len(buttons))]
|
||||||
if len(storageButtons) == 1 {
|
markup.Rows = append(markup.Rows, row)
|
||||||
return &tg.ReplyInlineMarkup{
|
|
||||||
Rows: []tg.KeyboardButtonRow{
|
|
||||||
{
|
|
||||||
Buttons: storageButtons,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &tg.ReplyInlineMarkup{
|
|
||||||
Rows: []tg.KeyboardButtonRow{
|
|
||||||
{
|
|
||||||
Buttons: storageButtons,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Buttons: []tg.KeyboardButtonClass{
|
|
||||||
&tg.KeyboardButtonCallback{
|
|
||||||
Text: "全部",
|
|
||||||
Data: []byte(fmt.Sprintf("add %d all", messageID)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
return markup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FileFromMedia(media tg.MessageMediaClass) (*types.File, error) {
|
func getSetDefaultStorageMarkup(userChatID int64, storages []storage.Storage) *tg.ReplyInlineMarkup {
|
||||||
|
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||||
|
for _, storage := range storages {
|
||||||
|
nameHash := common.HashString(storage.Name())
|
||||||
|
storageHashName[nameHash] = storage.Name()
|
||||||
|
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
||||||
|
Text: storage.Name(),
|
||||||
|
Data: []byte(fmt.Sprintf("set_default %d %s", userChatID, nameHash)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
markup := &tg.ReplyInlineMarkup{}
|
||||||
|
for i := 0; i < len(buttons); i += 3 {
|
||||||
|
row := tg.KeyboardButtonRow{}
|
||||||
|
row.Buttons = buttons[i:min(i+3, len(buttons))]
|
||||||
|
markup.Rows = append(markup.Rows, row)
|
||||||
|
}
|
||||||
|
return markup
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileFromMedia(media tg.MessageMediaClass, customFileName string) (*types.File, error) {
|
||||||
switch media := media.(type) {
|
switch media := media.(type) {
|
||||||
case *tg.MessageMediaDocument:
|
case *tg.MessageMediaDocument:
|
||||||
document, ok := media.Document.AsNotEmpty()
|
document, ok := media.Document.AsNotEmpty()
|
||||||
if !ok {
|
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 {
|
for _, attribute := range document.Attributes {
|
||||||
if name, ok := attribute.(*tg.DocumentAttributeFilename); ok {
|
if name, ok := attribute.(*tg.DocumentAttributeFilename); ok {
|
||||||
fileName = name.GetFileName()
|
fileName = name.GetFileName()
|
||||||
break
|
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{
|
return &types.File{
|
||||||
Location: document.AsInputDocumentFileLocation(),
|
Location: document.AsInputDocumentFileLocation(),
|
||||||
FileSize: document.Size,
|
FileSize: document.Size,
|
||||||
@@ -100,33 +117,37 @@ func FileFromMedia(media tg.MessageMediaClass) (*types.File, error) {
|
|||||||
case *tg.MessageMediaPhoto:
|
case *tg.MessageMediaPhoto:
|
||||||
photo, ok := media.Photo.AsNotEmpty()
|
photo, ok := media.Photo.AsNotEmpty()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("photo is empty")
|
return nil, ErrEmptyPhoto
|
||||||
}
|
}
|
||||||
sizes := photo.Sizes
|
sizes := photo.Sizes
|
||||||
if len(sizes) == 0 {
|
if len(sizes) == 0 {
|
||||||
return nil, fmt.Errorf("photo sizes is empty")
|
return nil, ErrEmptyPhotoSizes
|
||||||
}
|
}
|
||||||
photoSize := sizes[len(sizes)-1]
|
photoSize := sizes[len(sizes)-1]
|
||||||
size, ok := photoSize.AsNotEmpty()
|
size, ok := photoSize.AsNotEmpty()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("photo size is empty")
|
return nil, ErrEmptyPhotoSize
|
||||||
}
|
}
|
||||||
location := new(tg.InputPhotoFileLocation)
|
location := new(tg.InputPhotoFileLocation)
|
||||||
location.ID = photo.GetID()
|
location.ID = photo.GetID()
|
||||||
location.AccessHash = photo.GetAccessHash()
|
location.AccessHash = photo.GetAccessHash()
|
||||||
location.FileReference = photo.GetFileReference()
|
location.FileReference = photo.GetFileReference()
|
||||||
location.ThumbSize = size.GetType()
|
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{
|
return &types.File{
|
||||||
Location: location,
|
Location: location,
|
||||||
FileSize: 0,
|
FileSize: 0,
|
||||||
FileName: fmt.Sprintf("photo_%s_%d.jpg", time.Now().Format("2006-01-02_15-04-05"), photo.GetID()),
|
FileName: fileName,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unexpected type %T", media)
|
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)
|
key := fmt.Sprintf("file:%d:%d", chatID, messageID)
|
||||||
logger.L.Debugf("Getting file: %s", key)
|
logger.L.Debugf("Getting file: %s", key)
|
||||||
var cachedFile types.File
|
var cachedFile types.File
|
||||||
@@ -134,12 +155,11 @@ func FileFromMessage(ctx context.Context, client *gotgproto.Client, chatID int64
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return &cachedFile, nil
|
return &cachedFile, nil
|
||||||
}
|
}
|
||||||
|
message, err := GetTGMessage(ctx, chatID, messageID)
|
||||||
message, err := GetTGMessage(ctx, client, messageID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
file, err := FileFromMedia(message.Media)
|
file, err := FileFromMedia(message.Media, customFileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -149,20 +169,76 @@ func FileFromMessage(ctx context.Context, client *gotgproto.Client, chatID int64
|
|||||||
return file, nil
|
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)
|
logger.L.Debugf("Fetching message: %d", messageID)
|
||||||
res, err := client.API().MessagesGetMessages(ctx, []tg.InputMessageClass{
|
messages, err := ctx.GetMessages(chatId, []tg.InputMessageClass{&tg.InputMessageID{ID: messageID}})
|
||||||
&tg.InputMessageID{
|
|
||||||
ID: messageID,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
messages := res.(*tg.MessagesMessages)
|
if len(messages) == 0 {
|
||||||
msg := messages.Messages[0]
|
return nil, errors.New("no messages found")
|
||||||
if _, ok := msg.(*tg.Message); !ok {
|
|
||||||
return nil, fmt.Errorf("unexpected type %T, this file may be deleted", msg)
|
|
||||||
}
|
}
|
||||||
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 int64, 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()
|
||||||
|
}
|
||||||
|
markup, err := getSelectStorageMarkup(update.EffectiveUser().GetID(), int(chatID), fileMsgID)
|
||||||
|
if errors.Is(err, ErrNoStorages) {
|
||||||
|
logger.L.Errorf("Failed to get select storage markup: %s", err)
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: "无可用存储",
|
||||||
|
ID: toEditMsgID,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
} else if err != nil {
|
||||||
|
logger.L.Errorf("Failed to get select storage markup: %s", err)
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: "无法获取存储",
|
||||||
|
ID: toEditMsgID,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: text,
|
||||||
|
Entities: entities,
|
||||||
|
ReplyMarkup: markup,
|
||||||
|
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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/bootstrap"
|
"github.com/krau/SaveAny-Bot/bootstrap"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/core"
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -18,5 +20,32 @@ func Run(_ *cobra.Command, _ []string) {
|
|||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
sig := <-quit
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
common/utils.go
Normal file
12
common/utils.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HashString(s string) string {
|
||||||
|
hash := md5.New()
|
||||||
|
hash.Write([]byte(s))
|
||||||
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
|
}
|
||||||
@@ -2,41 +2,83 @@ workers = 4 # 同时下载文件数
|
|||||||
retry = 3 # 下载失败重试次数
|
retry = 3 # 下载失败重试次数
|
||||||
|
|
||||||
[telegram]
|
[telegram]
|
||||||
token = "" # Bot Token
|
# Bot Token
|
||||||
admins = [777000] # 你的 user_id
|
token = ""
|
||||||
app_id = 123456 # Telegram API ID
|
# Telegram API 配置, 若不配置也可运行, 将使用默认的 API ID 和 API HASH
|
||||||
app_hash = "0123456789abcdef0123456789abcdef" # Telegram API Hash
|
# 推荐使用自己的 API ID 和 API HASH (https://my.telegram.org)
|
||||||
|
# app_id = 123456
|
||||||
|
# app_hash = "0123456789abcdef0123456789abcdef"
|
||||||
|
|
||||||
[telegram.proxy]
|
[telegram.proxy]
|
||||||
|
# 启用代理连接 telegram, 只支持 socks5
|
||||||
enable = false
|
enable = false
|
||||||
url = "socks5://127.0.0.1:7890" # 代理地址
|
url = "socks5://127.0.0.1:7890"
|
||||||
|
|
||||||
[log]
|
|
||||||
level = "DEBUG" # 日志等级
|
|
||||||
|
|
||||||
[temp]
|
# 存储配置列表
|
||||||
base_path = "cache/" # 下载文件临时目录, 请不要在此目录下存放任何其他文件
|
[[storages]]
|
||||||
cache_ttl = 30 # 临时文件保存时间, 单位: 秒
|
# 标识名, 需要唯一
|
||||||
|
name = "本机1"
|
||||||
[db]
|
# 存储类型, 目前可用: local , alist , webdav
|
||||||
path = "data/data.db" # 数据库文件路径
|
type = "local"
|
||||||
|
|
||||||
[storage]
|
|
||||||
[storage.alist] # Alist
|
|
||||||
enable = true
|
enable = true
|
||||||
base_path = "/telegram" # 保存路径
|
base_path = "./downloads"
|
||||||
username = "admin" # 用户名
|
|
||||||
password = "password" # 密码
|
|
||||||
url = "https://alist.com" # Alist 地址
|
|
||||||
token_exp = 86400 # token 过期时间, 单位: 秒
|
|
||||||
|
|
||||||
[storage.local] # 本地磁盘
|
[[storages]]
|
||||||
|
name = "本机2"
|
||||||
|
type = "local"
|
||||||
enable = true
|
enable = true
|
||||||
base_path = "downloads/" # 保存路径
|
base_path = "./downloads/2"
|
||||||
|
|
||||||
[storage.webdav] # WebDav
|
[[storages]]
|
||||||
|
name = "MyAlist"
|
||||||
|
type = "alist"
|
||||||
enable = true
|
enable = true
|
||||||
base_path = "/telegram"
|
base_path = '/'
|
||||||
username = "admin"
|
url = 'https://alist.com'
|
||||||
password = "password"
|
username = 'admin'
|
||||||
url = "https://alist.com/dav"
|
password = 'password'
|
||||||
|
token_exp = 86400
|
||||||
|
# alist 可直接使用 token 登录, 此时 username, password, token_exp 将被忽略
|
||||||
|
# 请自行在 alist 侧配置合理的 token 过期时间
|
||||||
|
# token = ""
|
||||||
|
|
||||||
|
|
||||||
|
[[storages]]
|
||||||
|
name = "MyWebdav"
|
||||||
|
type = "webdav"
|
||||||
|
base_path = '/path/telegram'
|
||||||
|
enable = true
|
||||||
|
url = 'https://example.com/dav'
|
||||||
|
username = 'username'
|
||||||
|
password = 'password'
|
||||||
|
|
||||||
|
|
||||||
|
# 用户列表
|
||||||
|
[[users]]
|
||||||
|
# user id
|
||||||
|
id = 123456
|
||||||
|
# 存储名称过滤列表
|
||||||
|
storages = ["MyAlist", "本机1"]
|
||||||
|
# 开启黑名单模式, 过滤列表中的存储将无法使用, 默认为白名单模式
|
||||||
|
blacklist = false
|
||||||
|
|
||||||
|
[[users]]
|
||||||
|
id = 114514
|
||||||
|
# 将列表留空并开启黑名单模式以允许使用所有存储
|
||||||
|
storages = []
|
||||||
|
blacklist = true
|
||||||
|
|
||||||
|
|
||||||
|
# [log]
|
||||||
|
# # 日志等级
|
||||||
|
# level = "DEBUG"
|
||||||
|
|
||||||
|
# [temp]
|
||||||
|
# # 下载文件临时目录, 请不要在此目录下存放任何其他文件
|
||||||
|
# base_path = "cache/"
|
||||||
|
# # 临时文件保存时间, 单位: 秒
|
||||||
|
# cache_ttl = 30
|
||||||
|
|
||||||
|
# [db]
|
||||||
|
# path = "data/data.db" # 数据库文件路径
|
||||||
|
|||||||
95
config/deprecated.go
Normal file
95
config/deprecated.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
"gorm.io/datatypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// for compatibility
|
||||||
|
type deprecatedStorageConfig struct {
|
||||||
|
Alist alistConfig `toml:"alist" mapstructure:"alist"`
|
||||||
|
Local localConfig `toml:"local" mapstructure:"local"`
|
||||||
|
Webdav webdavConfig `toml:"webdav" mapstructure:"webdav"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type alistConfig struct {
|
||||||
|
Enable bool `toml:"enable" mapstructure:"enable" json:"enable"`
|
||||||
|
URL string `toml:"url" mapstructure:"url" json:"url"`
|
||||||
|
Username string `toml:"username" mapstructure:"username" json:"username"`
|
||||||
|
Password string `toml:"password" mapstructure:"password" json:"password"`
|
||||||
|
Token string `toml:"token" mapstructure:"token" json:"token"`
|
||||||
|
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||||
|
TokenExp int64 `toml:"token_exp" mapstructure:"token_exp" json:"token_exp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *alistConfig) ToJSON() datatypes.JSON {
|
||||||
|
tokenExp := strconv.FormatInt(a.TokenExp, 10)
|
||||||
|
return datatypes.JSON([]byte(`{"url":"` + a.URL + `","username":"` + a.Username + `","password":"` + a.Password + `","token":"` + a.Token + `","base_path":"` + a.BasePath + `","token_exp":` + tokenExp + `}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
type localConfig struct {
|
||||||
|
Enable bool `toml:"enable" mapstructure:"enable" json:"enable"`
|
||||||
|
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localConfig) ToJSON() datatypes.JSON {
|
||||||
|
return datatypes.JSON([]byte(`{"base_path":"` + l.BasePath + `"}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
type webdavConfig struct {
|
||||||
|
Enable bool `toml:"enable" mapstructure:"enable" json:"enable"`
|
||||||
|
URL string `toml:"url" mapstructure:"url" json:"url"`
|
||||||
|
Username string `toml:"username" mapstructure:"username" json:"username"`
|
||||||
|
Password string `toml:"password" mapstructure:"password" json:"password"`
|
||||||
|
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webdavConfig) ToJSON() datatypes.JSON {
|
||||||
|
return datatypes.JSON([]byte(`{"url":"` + w.URL + `","username":"` + w.Username + `","password":"` + w.Password + `","base_path":"` + w.BasePath + `"}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformDeprecatedStorageConfig() {
|
||||||
|
if Cfg.DeprecatedStorage.Alist.Enable {
|
||||||
|
alistStorage := &AlistStorageConfig{
|
||||||
|
NewStorageConfig: NewStorageConfig{
|
||||||
|
Name: "Alist",
|
||||||
|
Enable: true,
|
||||||
|
Type: string(types.StorageTypeAlist),
|
||||||
|
},
|
||||||
|
URL: Cfg.DeprecatedStorage.Alist.URL,
|
||||||
|
Username: Cfg.DeprecatedStorage.Alist.Username,
|
||||||
|
Password: Cfg.DeprecatedStorage.Alist.Password,
|
||||||
|
Token: Cfg.DeprecatedStorage.Alist.Token,
|
||||||
|
BasePath: Cfg.DeprecatedStorage.Alist.BasePath,
|
||||||
|
TokenExp: Cfg.DeprecatedStorage.Alist.TokenExp,
|
||||||
|
}
|
||||||
|
Cfg.Storages = append(Cfg.Storages, alistStorage)
|
||||||
|
}
|
||||||
|
if Cfg.DeprecatedStorage.Local.Enable {
|
||||||
|
localStorage := &LocalStorageConfig{
|
||||||
|
NewStorageConfig: NewStorageConfig{
|
||||||
|
Name: "Local",
|
||||||
|
Enable: true,
|
||||||
|
Type: string(types.StorageTypeLocal),
|
||||||
|
},
|
||||||
|
BasePath: Cfg.DeprecatedStorage.Local.BasePath,
|
||||||
|
}
|
||||||
|
Cfg.Storages = append(Cfg.Storages, localStorage)
|
||||||
|
}
|
||||||
|
if Cfg.DeprecatedStorage.Webdav.Enable {
|
||||||
|
webdavStorage := &WebdavStorageConfig{
|
||||||
|
NewStorageConfig: NewStorageConfig{
|
||||||
|
Name: "Webdav",
|
||||||
|
Enable: true,
|
||||||
|
Type: string(types.StorageTypeWebdav),
|
||||||
|
},
|
||||||
|
URL: Cfg.DeprecatedStorage.Webdav.URL,
|
||||||
|
Username: Cfg.DeprecatedStorage.Webdav.Username,
|
||||||
|
Password: Cfg.DeprecatedStorage.Webdav.Password,
|
||||||
|
BasePath: Cfg.DeprecatedStorage.Webdav.BasePath,
|
||||||
|
}
|
||||||
|
Cfg.Storages = append(Cfg.Storages, webdavStorage)
|
||||||
|
}
|
||||||
|
}
|
||||||
104
config/storage_factory.go
Normal file
104
config/storage_factory.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// storage_config.go
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StorageConfig interface {
|
||||||
|
Validate() error
|
||||||
|
GetType() types.StorageType
|
||||||
|
GetName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base storage config
|
||||||
|
type NewStorageConfig struct {
|
||||||
|
Name string `toml:"name" mapstructure:"name" json:"name"`
|
||||||
|
Type string `toml:"type" mapstructure:"type" json:"type"`
|
||||||
|
Enable bool `toml:"enable" mapstructure:"enable" json:"enable"`
|
||||||
|
RawConfig map[string]interface{} `toml:"-" mapstructure:",remain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageConfigFactory func(cfg *NewStorageConfig) (StorageConfig, error)
|
||||||
|
|
||||||
|
var storageFactories = make(map[string]StorageConfigFactory)
|
||||||
|
|
||||||
|
func RegisterStorageFactory(storageType string, factory StorageConfigFactory) {
|
||||||
|
storageFactories[storageType] = factory
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterStorageFactory(string(types.StorageTypeLocal), newLocalStorageConfig)
|
||||||
|
RegisterStorageFactory(string(types.StorageTypeAlist), newAlistStorageConfig)
|
||||||
|
RegisterStorageFactory(string(types.StorageTypeWebdav), newWebdavStorageConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLocalStorageConfig(cfg *NewStorageConfig) (StorageConfig, error) {
|
||||||
|
var localCfg LocalStorageConfig
|
||||||
|
localCfg.NewStorageConfig = *cfg
|
||||||
|
|
||||||
|
if err := mapstructure.Decode(cfg.RawConfig, &localCfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode local storage config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &localCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAlistStorageConfig(cfg *NewStorageConfig) (StorageConfig, error) {
|
||||||
|
var alistCfg AlistStorageConfig
|
||||||
|
alistCfg.NewStorageConfig = *cfg
|
||||||
|
|
||||||
|
if err := mapstructure.Decode(cfg.RawConfig, &alistCfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode alist storage config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &alistCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWebdavStorageConfig(cfg *NewStorageConfig) (StorageConfig, error) {
|
||||||
|
var webdavCfg WebdavStorageConfig
|
||||||
|
webdavCfg.NewStorageConfig = *cfg
|
||||||
|
|
||||||
|
if err := mapstructure.Decode(cfg.RawConfig, &webdavCfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode webdav storage config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &webdavCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadStorageConfigs(v *viper.Viper) ([]StorageConfig, error) {
|
||||||
|
var baseConfigs []NewStorageConfig
|
||||||
|
if err := v.UnmarshalKey("storages", &baseConfigs); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal storage configs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var configs []StorageConfig
|
||||||
|
for _, baseCfg := range baseConfigs {
|
||||||
|
if !baseCfg.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
factory, ok := storageFactories[baseCfg.Type]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unsupported storage type: %s", baseCfg.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := factory(&baseCfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create storage config for %s: %w", baseCfg.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid storage config for %s: %w", baseCfg.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configs = append(configs, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs, nil
|
||||||
|
}
|
||||||
106
config/storages.go
Normal file
106
config/storages.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Config) GetStoragesByType(storageType types.StorageType) []StorageConfig {
|
||||||
|
var storages []StorageConfig
|
||||||
|
for _, storage := range c.Storages {
|
||||||
|
if storage.GetType() == storageType {
|
||||||
|
storages = append(storages, storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return storages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetStorageByName(name string) StorageConfig {
|
||||||
|
for _, storage := range c.Storages {
|
||||||
|
if storage.GetName() == name {
|
||||||
|
return storage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalStorageConfig struct {
|
||||||
|
NewStorageConfig
|
||||||
|
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalStorageConfig) Validate() error {
|
||||||
|
if l.BasePath == "" {
|
||||||
|
return fmt.Errorf("path is required for local storage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalStorageConfig) GetType() types.StorageType {
|
||||||
|
return types.StorageTypeLocal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalStorageConfig) GetName() string {
|
||||||
|
return l.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlistStorageConfig struct {
|
||||||
|
NewStorageConfig
|
||||||
|
URL string `toml:"url" mapstructure:"url" json:"url"`
|
||||||
|
Username string `toml:"username" mapstructure:"username" json:"username"`
|
||||||
|
Password string `toml:"password" mapstructure:"password" json:"password"`
|
||||||
|
Token string `toml:"token" mapstructure:"token" json:"token"`
|
||||||
|
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||||
|
TokenExp int64 `toml:"token_exp" mapstructure:"token_exp" json:"token_exp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AlistStorageConfig) Validate() error {
|
||||||
|
if a.URL == "" {
|
||||||
|
return fmt.Errorf("url is required for alist storage")
|
||||||
|
}
|
||||||
|
if a.Token == "" && (a.Username == "" || a.Password == "") {
|
||||||
|
return fmt.Errorf("username and password or token is required for alist storage")
|
||||||
|
}
|
||||||
|
if a.BasePath == "" {
|
||||||
|
return fmt.Errorf("base_path is required for alist storage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AlistStorageConfig) GetType() types.StorageType {
|
||||||
|
return types.StorageTypeAlist
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AlistStorageConfig) GetName() string {
|
||||||
|
return a.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebdavStorageConfig struct {
|
||||||
|
NewStorageConfig
|
||||||
|
URL string `toml:"url" mapstructure:"url" json:"url"`
|
||||||
|
Username string `toml:"username" mapstructure:"username" json:"username"`
|
||||||
|
Password string `toml:"password" mapstructure:"password" json:"password"`
|
||||||
|
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebdavStorageConfig) Validate() error {
|
||||||
|
if w.URL == "" {
|
||||||
|
return fmt.Errorf("url is required for webdav storage")
|
||||||
|
}
|
||||||
|
if w.Username == "" || w.Password == "" {
|
||||||
|
return fmt.Errorf("username and password is required for webdav storage")
|
||||||
|
}
|
||||||
|
if w.BasePath == "" {
|
||||||
|
return fmt.Errorf("base_path is required for webdav storage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebdavStorageConfig) GetType() types.StorageType {
|
||||||
|
return types.StorageTypeWebdav
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebdavStorageConfig) GetName() string {
|
||||||
|
return w.Name
|
||||||
|
}
|
||||||
49
config/user.go
Normal file
49
config/user.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userConfig struct {
|
||||||
|
ID int64 `toml:"id" mapstructure:"id" json:"id"` // telegram user id
|
||||||
|
Storages []string `toml:"storages" mapstructure:"storages" json:"storages"` // storage names
|
||||||
|
Blacklist bool `toml:"blacklist" mapstructure:"blacklist" json:"blacklist"` // 黑名单模式, storage names 中的存储将不会被使用, 默认为白名单模式
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetStorageNamesByUserID(userID int64) []string {
|
||||||
|
for _, user := range c.Users {
|
||||||
|
if user.ID == userID {
|
||||||
|
if user.Blacklist {
|
||||||
|
allStorages := make([]string, 0, len(c.Storages))
|
||||||
|
for _, storage := range c.Storages {
|
||||||
|
allStorages = append(allStorages, storage.GetName())
|
||||||
|
}
|
||||||
|
return slice.Compact(slice.Difference(allStorages, user.Storages))
|
||||||
|
} else {
|
||||||
|
return user.Storages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetUsersID() []int64 {
|
||||||
|
var ids []int64
|
||||||
|
for _, user := range c.Users {
|
||||||
|
ids = append(ids, user.ID)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) HasStorage(userID int64, storageName string) bool {
|
||||||
|
for _, user := range c.Users {
|
||||||
|
if user.ID == userID {
|
||||||
|
if user.Blacklist {
|
||||||
|
return !slice.Contain(user.Storages, storageName)
|
||||||
|
} else {
|
||||||
|
return slice.Contain(user.Storages, storageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
143
config/viper.go
143
config/viper.go
@@ -9,25 +9,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Workers int `toml:"workers" mapstructure:"workers"`
|
Workers int `toml:"workers" mapstructure:"workers"`
|
||||||
Retry int `toml:"retry" mapstructure:"retry"`
|
Retry int `toml:"retry" mapstructure:"retry"`
|
||||||
|
NoCleanCache bool `toml:"no_clean_cache" mapstructure:"no_clean_cache" json:"no_clean_cache"`
|
||||||
|
Users []userConfig `toml:"users" mapstructure:"users" json:"users"`
|
||||||
|
|
||||||
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
||||||
Log logConfig `toml:"log" mapstructure:"log"`
|
Log logConfig `toml:"log" mapstructure:"log"`
|
||||||
DB dbConfig `toml:"db" mapstructure:"db"`
|
DB dbConfig `toml:"db" mapstructure:"db"`
|
||||||
Telegram telegramConfig `toml:"telegram" mapstructure:"telegram"`
|
Telegram telegramConfig `toml:"telegram" mapstructure:"telegram"`
|
||||||
Storage storageConfig `toml:"storage" mapstructure:"storage"`
|
Storages []StorageConfig `toml:"-" mapstructure:"-" json:"storages"`
|
||||||
|
// Deprecated
|
||||||
|
DeprecatedStorage deprecatedStorageConfig `toml:"storage" mapstructure:"storage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type tempConfig struct {
|
type tempConfig struct {
|
||||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||||
CacheTTL int64 `toml:"cache_ttl" mapstructure:"cache_ttl"`
|
CacheTTL int64 `toml:"cache_ttl" mapstructure:"cache_ttl" json:"cache_ttl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type logConfig struct {
|
type logConfig struct {
|
||||||
Level string `toml:"level" mapstructure:"level"`
|
Level string `toml:"level" mapstructure:"level"`
|
||||||
File string `toml:"file" mapstructure:"file"`
|
File string `toml:"file" mapstructure:"file"`
|
||||||
BackupCount uint `toml:"backup_count" mapstructure:"backup_count"`
|
BackupCount uint `toml:"backup_count" mapstructure:"backup_count" json:"backup_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type dbConfig struct {
|
type dbConfig struct {
|
||||||
@@ -36,10 +40,12 @@ type dbConfig struct {
|
|||||||
|
|
||||||
type telegramConfig struct {
|
type telegramConfig struct {
|
||||||
Token string `toml:"token" mapstructure:"token"`
|
Token string `toml:"token" mapstructure:"token"`
|
||||||
AppID int `toml:"app_id" mapstructure:"app_id"`
|
AppID int `toml:"app_id" mapstructure:"app_id" json:"app_id"`
|
||||||
AppHash string `toml:"app_hash" mapstructure:"app_hash"`
|
AppHash string `toml:"app_hash" mapstructure:"app_hash" json:"app_hash"`
|
||||||
Admins []int64 `toml:"admins" mapstructure:"admins"`
|
|
||||||
Proxy proxyConfig `toml:"proxy" mapstructure:"proxy"`
|
Proxy proxyConfig `toml:"proxy" mapstructure:"proxy"`
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
|
Admins []int64 `toml:"admins" mapstructure:"admins"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type proxyConfig struct {
|
type proxyConfig struct {
|
||||||
@@ -47,37 +53,9 @@ type proxyConfig struct {
|
|||||||
URL string `toml:"url" mapstructure:"url"`
|
URL string `toml:"url" mapstructure:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type storageConfig struct {
|
|
||||||
Alist alistConfig `toml:"alist" mapstructure:"alist"`
|
|
||||||
Local localConfig `toml:"local" mapstructure:"local"`
|
|
||||||
Webdav webdavConfig `toml:"webdav" mapstructure:"webdav"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type alistConfig struct {
|
|
||||||
Enable bool `toml:"enable" mapstructure:"enable"`
|
|
||||||
URL string `toml:"url" mapstructure:"url"`
|
|
||||||
Username string `toml:"username" mapstructure:"username"`
|
|
||||||
Password string `toml:"password" mapstructure:"password"`
|
|
||||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
|
||||||
TokenExp int64 `toml:"token_exp" mapstructure:"token_exp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type localConfig struct {
|
|
||||||
Enable bool `toml:"enable" mapstructure:"enable"`
|
|
||||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type webdavConfig struct {
|
|
||||||
Enable bool `toml:"enable" mapstructure:"enable"`
|
|
||||||
URL string `toml:"url" mapstructure:"url"`
|
|
||||||
Username string `toml:"username" mapstructure:"username"`
|
|
||||||
Password string `toml:"password" mapstructure:"password"`
|
|
||||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var Cfg *Config
|
var Cfg *Config
|
||||||
|
|
||||||
func Init() {
|
func Init() error {
|
||||||
viper.SetConfigName("config")
|
viper.SetConfigName("config")
|
||||||
viper.AddConfigPath(".")
|
viper.AddConfigPath(".")
|
||||||
viper.AddConfigPath("/etc/saveany/")
|
viper.AddConfigPath("/etc/saveany/")
|
||||||
@@ -90,6 +68,9 @@ func Init() {
|
|||||||
viper.SetDefault("workers", 3)
|
viper.SetDefault("workers", 3)
|
||||||
viper.SetDefault("retry", 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.base_path", "cache/")
|
||||||
viper.SetDefault("temp.cache_ttl", 3600)
|
viper.SetDefault("temp.cache_ttl", 3600)
|
||||||
|
|
||||||
@@ -99,8 +80,11 @@ func Init() {
|
|||||||
|
|
||||||
viper.SetDefault("db.path", "data/saveany.db")
|
viper.SetDefault("db.path", "data/saveany.db")
|
||||||
|
|
||||||
viper.SetDefault("storage.alist.base_path", "/")
|
if err := viper.SafeWriteConfigAs("config.toml"); err != nil {
|
||||||
viper.SetDefault("storage.alist.token_exp", 3600)
|
if _, ok := err.(viper.ConfigFileAlreadyExistsError); !ok {
|
||||||
|
return fmt.Errorf("error saving default config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
fmt.Println("Error reading config file, ", err)
|
fmt.Println("Error reading config file, ", err)
|
||||||
@@ -108,12 +92,77 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Cfg = &Config{}
|
Cfg = &Config{}
|
||||||
|
|
||||||
if err := viper.Unmarshal(Cfg); err != nil {
|
if err := viper.Unmarshal(Cfg); err != nil {
|
||||||
fmt.Println("Error unmarshalling config file, ", err)
|
fmt.Println("Error unmarshalling config file, ", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if Cfg.Workers < 1 || Cfg.Retry < 1 {
|
|
||||||
fmt.Println("Invalid workers or retry value")
|
if Cfg.Telegram.Admins != nil {
|
||||||
os.Exit(1)
|
fmt.Println("警告: 你正在使用旧版 Telegram 管理员配置, 该配置下的用户将可用所有存储.\ntelegram.admins 未来版本将会被废弃, 请参考新的配置文件模板, 使用 users 配置替代.")
|
||||||
|
for _, admin := range Cfg.Telegram.Admins {
|
||||||
|
found := false
|
||||||
|
for _, user := range Cfg.Users {
|
||||||
|
if user.ID == admin {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Cfg.Users = append(Cfg.Users, userConfig{
|
||||||
|
ID: admin,
|
||||||
|
Storages: []string{},
|
||||||
|
Blacklist: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storagesConfig, err := LoadStorageConfigs(viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error loading storage configs: %w", err)
|
||||||
|
}
|
||||||
|
Cfg.Storages = storagesConfig
|
||||||
|
|
||||||
|
if Cfg.DeprecatedStorage != (deprecatedStorageConfig{}) {
|
||||||
|
fmt.Println("\n警告: 你正在使用旧版存储配置, 未来版本将会被废弃.\n请参考新的配置文件模板.")
|
||||||
|
transformDeprecatedStorageConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
storageNames := make(map[string]struct{})
|
||||||
|
for _, storage := range Cfg.Storages {
|
||||||
|
if _, ok := storageNames[storage.GetName()]; ok {
|
||||||
|
return fmt.Errorf("重复的存储名: %s", storage.GetName())
|
||||||
|
}
|
||||||
|
storageNames[storage.GetName()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("已加载 %d 个存储:\n", len(Cfg.Storages))
|
||||||
|
for _, storage := range Cfg.Storages {
|
||||||
|
fmt.Printf(" - %s (%s)\n", storage.GetName(), storage.GetType())
|
||||||
|
}
|
||||||
|
|
||||||
|
if Cfg.Workers < 1 || Cfg.Retry < 1 {
|
||||||
|
return fmt.Errorf("workers 和 retry 必须大于 0, 当前值: workers=%d, retry=%d", Cfg.Workers, Cfg.Retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
114
core/core.go
114
core/core.go
@@ -6,58 +6,52 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/duke-git/lancet/v2/fileutil"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/bot"
|
"github.com/krau/SaveAny-Bot/bot"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
"github.com/krau/SaveAny-Bot/queue"
|
"github.com/krau/SaveAny-Bot/queue"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func processPendingTask(task *types.Task) error {
|
func processPendingTask(task *types.Task) error {
|
||||||
logger.L.Debugf("Start processing task: %s", task.String())
|
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("处理路径失败: %w", err)
|
||||||
|
}
|
||||||
|
if err := fileutil.CreateDir(filepath.Dir(cacheDestPath)); err != nil {
|
||||||
|
return fmt.Errorf("创建目录失败: %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 == "" {
|
if task.StoragePath == "" {
|
||||||
task.StoragePath = task.File.FileName
|
task.StoragePath = task.File.FileName
|
||||||
}
|
}
|
||||||
|
|
||||||
// process photo
|
taskStorage, err := storage.GetStorageByUserIDAndName(task.UserID, task.StorageName)
|
||||||
if task.File.FileSize == 0 {
|
if err != nil {
|
||||||
res, err := bot.Client.API().UploadGetFile(task.Ctx, &tg.UploadGetFileRequest{
|
return err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
task.StoragePath = taskStorage.JoinStoragePath(*task)
|
||||||
|
|
||||||
|
if task.File.FileSize == 0 {
|
||||||
|
return processPhoto(task, taskStorage, cacheDestPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := task.Ctx.(*ext.Context)
|
||||||
|
|
||||||
barTotalCount := calculateBarTotalCount(task.File.FileSize)
|
barTotalCount := calculateBarTotalCount(task.File.FileSize)
|
||||||
|
|
||||||
@@ -67,44 +61,56 @@ func processPendingTask(task *types.Task) error {
|
|||||||
if task.File.FileSize < 1024*1024*50 || int(progress)%(100/barTotalCount) != 0 {
|
if task.File.FileSize < 1024*1024*50 || int(progress)%(100/barTotalCount) != 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
text := fmt.Sprintf("正在下载: %s\n[%s] %.2f%%",
|
text, entities := buildProgressMessageEntity(task, barTotalCount, bytesRead, task.StartTime, progress)
|
||||||
task.String(),
|
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
getProgressBar(progress, barTotalCount),
|
Message: text,
|
||||||
progress,
|
Entities: entities,
|
||||||
)
|
ID: task.ReplyMessageID,
|
||||||
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
|
||||||
Message: text,
|
|
||||||
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,
|
readCloser, err := NewTelegramReader(task.Ctx, bot.Client, &task.File.Location,
|
||||||
0, task.File.FileSize-1, task.File.FileSize,
|
0, task.File.FileSize-1, task.File.FileSize,
|
||||||
progressCallback, task.File.FileSize/100)
|
progressCallback, task.File.FileSize/100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to create reader: %w", err)
|
return fmt.Errorf("创建下载失败: %w", err)
|
||||||
}
|
}
|
||||||
defer readCloser.Close()
|
defer readCloser.Close()
|
||||||
|
|
||||||
dest, err := os.Create(destPath)
|
dest, err := os.Create(cacheDestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to create file: %w", err)
|
return fmt.Errorf("创建文件失败: %w", err)
|
||||||
}
|
}
|
||||||
defer dest.Close()
|
defer dest.Close()
|
||||||
|
task.StartTime = time.Now()
|
||||||
if _, err := io.CopyN(dest, readCloser, task.File.FileSize); err != nil {
|
if _, err := io.CopyN(dest, readCloser, task.File.FileSize); err != nil {
|
||||||
return fmt.Errorf("Failed to download file: %w", err)
|
return fmt.Errorf("下载文件失败: %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", cacheDestPath)
|
||||||
|
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
logger.L.Infof("Downloaded file: %s", destPath)
|
|
||||||
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
|
||||||
Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()),
|
Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()),
|
||||||
ID: task.ReplyMessageID,
|
ID: task.ReplyMessageID,
|
||||||
})
|
})
|
||||||
|
|
||||||
return saveFileWithRetry(task, destPath)
|
return saveFileWithRetry(task, taskStorage, cacheDestPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
||||||
@@ -131,13 +137,13 @@ func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
|||||||
queue.AddTask(task)
|
queue.AddTask(task)
|
||||||
case types.Succeeded:
|
case types.Succeeded:
|
||||||
logger.L.Infof("Task succeeded: %s", task.String())
|
logger.L.Infof("Task succeeded: %s", task.String())
|
||||||
task.Ctx.(*ext.Context).EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
task.Ctx.(*ext.Context).EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
Message: "保存成功\n" + task.FileName(),
|
Message: fmt.Sprintf("文件保存成功\n [%s]: %s", task.StorageName, task.StoragePath),
|
||||||
ID: task.ReplyMessageID,
|
ID: task.ReplyMessageID,
|
||||||
})
|
})
|
||||||
case types.Failed:
|
case types.Failed:
|
||||||
logger.L.Errorf("Task failed: %s", task.String())
|
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(),
|
Message: "文件保存失败\n" + task.Error.Error(),
|
||||||
ID: task.ReplyMessageID,
|
ID: task.ReplyMessageID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"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/common"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
@@ -12,11 +16,11 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func saveFileWithRetry(task *types.Task, destPath string) error {
|
func saveFileWithRetry(task *types.Task, taskStorage storage.Storage, localFilePath string) error {
|
||||||
for i := 0; i <= config.Cfg.Retry; i++ {
|
for i := 0; i <= config.Cfg.Retry; i++ {
|
||||||
if err := storage.Save(task.Storage, task.Ctx, destPath, task.StoragePath); err != nil {
|
if err := taskStorage.Save(task.Ctx, localFilePath, task.StoragePath); err != nil {
|
||||||
if i == config.Cfg.Retry {
|
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)
|
logger.L.Errorf("Failed to save file: %s, retrying...", err)
|
||||||
continue
|
continue
|
||||||
@@ -26,6 +30,32 @@ func saveFileWithRetry(task *types.Task, destPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func processPhoto(task *types.Task, taskStorage storage.Storage, 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, taskStorage, cachePath)
|
||||||
|
}
|
||||||
|
|
||||||
func getProgressBar(progress float64, totalCount int) string {
|
func getProgressBar(progress float64, totalCount int) string {
|
||||||
bar := ""
|
bar := ""
|
||||||
barSize := 100 / totalCount
|
barSize := 100 / totalCount
|
||||||
@@ -52,7 +82,7 @@ func cleanCacheFile(destPath string) {
|
|||||||
func calculateBarTotalCount(fileSize int64) int {
|
func calculateBarTotalCount(fileSize int64) int {
|
||||||
barTotalCount := 5
|
barTotalCount := 5
|
||||||
if fileSize > 1024*1024*1000 {
|
if fileSize > 1024*1024*1000 {
|
||||||
barTotalCount = 50
|
barTotalCount = 40
|
||||||
} else if fileSize > 1024*1024*500 {
|
} else if fileSize > 1024*1024*500 {
|
||||||
barTotalCount = 20
|
barTotalCount = 20
|
||||||
} else if fileSize > 1024*1024*200 {
|
} else if fileSize > 1024*1024*200 {
|
||||||
@@ -60,3 +90,38 @@ func calculateBarTotalCount(fileSize int64) int {
|
|||||||
}
|
}
|
||||||
return barTotalCount
|
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.StorageName, 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.StorageName, 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()
|
||||||
|
}
|
||||||
|
|||||||
19
dao/db.go
19
dao/db.go
@@ -3,29 +3,42 @@ package dao
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
glogger "gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
if err := os.MkdirAll(filepath.Dir(config.Cfg.DB.Path), 755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(config.Cfg.DB.Path), 0755); err != nil {
|
||||||
logger.L.Fatal("Failed to create data directory: ", err)
|
logger.L.Fatal("Failed to create data directory: ", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
logger.L.Fatal("Failed to open database: ", err)
|
logger.L.Fatal("Failed to open database: ", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
logger.L.Debug("Database connected")
|
logger.L.Debug("Database connected")
|
||||||
db.AutoMigrate(&types.ReceivedFile{}, &types.User{})
|
if err := db.AutoMigrate(&types.ReceivedFile{}, &types.User{}); err != nil {
|
||||||
|
logger.L.Fatal("迁移数据库失败, 如果您从旧版本升级, 建议手动删除数据库文件后重试: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, admin := range config.Cfg.Telegram.Admins {
|
for _, admin := range config.Cfg.Telegram.Admins {
|
||||||
CreateUser(int64(admin))
|
CreateUser(int64(admin))
|
||||||
|
|||||||
12
dao/file.go
12
dao/file.go
@@ -2,8 +2,12 @@ package dao
|
|||||||
|
|
||||||
import "github.com/krau/SaveAny-Bot/types"
|
import "github.com/krau/SaveAny-Bot/types"
|
||||||
|
|
||||||
func AddReceivedFile(receivedFile *types.ReceivedFile) error {
|
func SaveReceivedFile(receivedFile *types.ReceivedFile) error {
|
||||||
return db.Create(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) {
|
func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*types.ReceivedFile, error) {
|
||||||
@@ -15,10 +19,6 @@ func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*types.Rece
|
|||||||
return &receivedFile, nil
|
return &receivedFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateReceivedFile(receivedFile *types.ReceivedFile) error {
|
|
||||||
return db.Save(receivedFile).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteReceivedFile(receivedFile *types.ReceivedFile) error {
|
func DeleteReceivedFile(receivedFile *types.ReceivedFile) error {
|
||||||
return db.Delete(receivedFile).Error
|
return db.Delete(receivedFile).Error
|
||||||
}
|
}
|
||||||
|
|||||||
20
dao/user.go
20
dao/user.go
@@ -4,19 +4,29 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateUser(userID int64) error {
|
func CreateUser(chatID int64) error {
|
||||||
if _, err := GetUserByUserID(userID); err == nil {
|
if _, err := GetUserByChatID(chatID); err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return db.Create(&types.User{UserID: userID}).Error
|
return db.Create(&types.User{ChatID: chatID}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserByUserID(userID int64) (*types.User, error) {
|
func GetAllUsers() ([]types.User, error) {
|
||||||
|
var users []types.User
|
||||||
|
err := db.Find(&users).Error
|
||||||
|
return users, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserByChatID(chatID int64) (*types.User, error) {
|
||||||
var user types.User
|
var user types.User
|
||||||
err := db.Where("user_id = ?", userID).First(&user).Error
|
err := db.Where("chat_id = ?", chatID).First(&user).Error
|
||||||
return &user, err
|
return &user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateUser(user *types.User) error {
|
func UpdateUser(user *types.User) error {
|
||||||
return db.Save(user).Error
|
return db.Save(user).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteUser(user *types.User) error {
|
||||||
|
return db.Delete(user).Error
|
||||||
|
}
|
||||||
|
|||||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
saveany-bot:
|
||||||
|
image: ghcr.io/krau/saveany-bot:latest
|
||||||
|
container_name: saveany-bot
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./config.toml:/app/config.toml
|
||||||
|
- ./downloads:/app/downloads
|
||||||
|
- ./cache:/app/cache
|
||||||
49
go.mod
49
go.mod
@@ -4,55 +4,50 @@ go 1.23.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blang/semver v3.5.1+incompatible
|
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/gookit/slog v0.5.7
|
||||||
github.com/gotd/contrib v0.21.0
|
github.com/gotd/contrib v0.21.0
|
||||||
github.com/gotd/td v0.118.0
|
github.com/gotd/td v0.120.0
|
||||||
github.com/imroc/req/v3 v3.49.1
|
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
github.com/studio-b12/gowebdav v0.10.0
|
github.com/studio-b12/gowebdav v0.10.0
|
||||||
golang.org/x/net v0.34.0
|
golang.org/x/net v0.35.0
|
||||||
golang.org/x/time v0.9.0
|
golang.org/x/time v0.10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/AnimeKaizoku/cacher v1.0.2 // indirect
|
github.com/AnimeKaizoku/cacher v1.0.2 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.5.0 // indirect
|
|
||||||
github.com/coder/websocket v1.8.12 // 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/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fatih/color v1.18.0 // 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/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-faster/errors v0.7.1 // indirect
|
github.com/go-faster/errors v0.7.1 // indirect
|
||||||
github.com/go-faster/jx v1.1.0 // indirect
|
github.com/go-faster/jx v1.1.0 // indirect
|
||||||
github.com/go-faster/xor v1.0.0 // indirect
|
github.com/go-faster/xor v1.0.0 // indirect
|
||||||
github.com/go-faster/yaml v0.4.6 // indirect
|
github.com/go-faster/yaml v0.4.6 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf // indirect
|
github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gotd/ige v0.2.2 // indirect
|
github.com/gotd/ige v0.2.2 // indirect
|
||||||
github.com/gotd/neo v0.1.5 // indirect
|
github.com/gotd/neo v0.1.5 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // 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/ginkgo/v2 v2.22.2 // indirect
|
github.com/onsi/gomega v1.36.2 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
|
||||||
github.com/quic-go/quic-go v0.49.0 // indirect
|
|
||||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/segmentio/asm v1.2.0 // indirect
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
||||||
@@ -61,22 +56,21 @@ require (
|
|||||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
golang.org/x/crypto v0.32.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
golang.org/x/mod v0.22.0 // indirect
|
golang.org/x/mod v0.23.0 // indirect
|
||||||
golang.org/x/oauth2 v0.25.0 // indirect
|
golang.org/x/oauth2 v0.26.0 // indirect
|
||||||
golang.org/x/tools v0.29.0 // indirect
|
golang.org/x/tools v0.30.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
modernc.org/libc v1.61.11 // indirect
|
gorm.io/driver/mysql v1.5.6 // indirect
|
||||||
|
modernc.org/libc v1.61.13 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.8.2 // 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
|
rsc.io/qr v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
|
||||||
github.com/coocood/freecache v1.2.4
|
github.com/coocood/freecache v1.2.4
|
||||||
github.com/duke-git/lancet/v2 v2.3.4
|
github.com/duke-git/lancet/v2 v2.3.4
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
@@ -100,11 +94,12 @@ require (
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
|
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gorm.io/datatypes v1.2.5
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.25.12
|
||||||
)
|
)
|
||||||
|
|||||||
74
go.sum
74
go.sum
@@ -1,18 +1,18 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/AnimeKaizoku/cacher v1.0.2 h1:7Bf5qRylWb7q2Evib0OXlhG37/t7BP2HK/7IyPvSmGQ=
|
github.com/AnimeKaizoku/cacher v1.0.2 h1:7Bf5qRylWb7q2Evib0OXlhG37/t7BP2HK/7IyPvSmGQ=
|
||||||
github.com/AnimeKaizoku/cacher v1.0.2/go.mod h1:jw0de/b0K6W7Y3T9rHCMGVKUf6oG7hENNcssxYcZTCc=
|
github.com/AnimeKaizoku/cacher v1.0.2/go.mod h1:jw0de/b0K6W7Y3T9rHCMGVKUf6oG7hENNcssxYcZTCc=
|
||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
|
||||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
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 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.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 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
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=
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
|
|
||||||
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
|
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
|
||||||
@@ -23,6 +23,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/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 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
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 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
|
||||||
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
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=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
@@ -34,6 +36,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.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 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
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 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
@@ -53,8 +57,9 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
|||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
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=
|
||||||
@@ -85,16 +90,11 @@ 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/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 h1:iPGkaOAd3QO72TcvzNJGKGpLDzYOW8GIz+Va2upxBbY=
|
||||||
github.com/gotd/td v0.118.0/go.mod h1:FUNVeJB9Id2Vqps9yF+8kmBNNyCGO6VXDyO8Ah7bVSw=
|
github.com/gotd/td v0.118.0/go.mod h1:FUNVeJB9Id2Vqps9yF+8kmBNNyCGO6VXDyO8Ah7bVSw=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/gotd/td v0.120.0 h1:XeiafJM82/9SaB+ZMjMm/dnUx5+avINwVZOEsnV0zMo=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/gotd/td v0.120.0/go.mod h1:BCc2jFj1l5zP9Trk4J7nxeqW0KBGl6K95eXMgszkbOI=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
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/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=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/imroc/req/v3 v3.49.1 h1:Nvwo02riiPEzh74ozFHeEJrtjakFxnoWNR3YZYuQm9U=
|
|
||||||
github.com/imroc/req/v3 v3.49.1/go.mod h1:tsOk8K7zI6cU4xu/VWCZVtq9Djw9IWm4MslKzme5woU=
|
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
@@ -124,9 +124,9 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
|||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
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 h1:n+lDQpiSFYC9G4hTvuNVWnqmIP0LR8ws0faDn9jX3hU=
|
||||||
github.com/ogen-go/ogen v1.9.0/go.mod h1:vkHpuRyzjdfuRCy81EShi4t9sIgZDcNPGmiDKipRloc=
|
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/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
|
|
||||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
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=
|
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||||
@@ -136,12 +136,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
|
||||||
github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94=
|
|
||||||
github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s=
|
|
||||||
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
|
||||||
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
|
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
|
||||||
@@ -183,8 +177,6 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
|||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||||
@@ -197,8 +189,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
|||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
@@ -207,47 +197,66 @@ 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.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 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
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 h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
|
||||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
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 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
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-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-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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
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-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
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.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-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.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 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
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.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.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 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
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 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
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.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 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
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=
|
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.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
|
||||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
@@ -261,18 +270,27 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
|
||||||
|
gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
|
||||||
|
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||||
|
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
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/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 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc=
|
||||||
modernc.org/ccgo/v4 v4.23.15/go.mod h1:nJX30dks/IWuBOnVa7VRii9Me4/9TZ1SC9GNtmARTy0=
|
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 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
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 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds=
|
||||||
modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
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 h1:6sZG8uB6EMMG7iTLPTndi8jyTdgAQNIeLGjCFICACZw=
|
||||||
modernc.org/libc v1.61.11/go.mod h1:HHX+srFdn839oaJRd0W8hBM3eg+mieyZCAjWwB08/nM=
|
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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
||||||
@@ -283,6 +301,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
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 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
|
||||||
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
|
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 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package alist
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -12,126 +11,141 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Alist struct{}
|
type Alist struct {
|
||||||
|
client *http.Client
|
||||||
var (
|
token string
|
||||||
basePath string
|
baseURL string
|
||||||
baseUrl string
|
loginInfo *loginRequest
|
||||||
reqClient *req.Client
|
config config.AlistStorageConfig
|
||||||
loginReq *loginRequset
|
|
||||||
|
|
||||||
ErrAlistLoginFailed = errors.New("failed to login to Alist")
|
|
||||||
)
|
|
||||||
|
|
||||||
type loginRequset struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type loginResponse struct {
|
func (a *Alist) Init(cfg config.StorageConfig) error {
|
||||||
Code int `json:"code"`
|
alistConfig, ok := cfg.(*config.AlistStorageConfig)
|
||||||
Message string `json:"message"`
|
if !ok {
|
||||||
Data struct {
|
return fmt.Errorf("failed to cast alist config")
|
||||||
Token string `json:"token"`
|
}
|
||||||
} `json:"data"`
|
if err := alistConfig.Validate(); err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
a.config = *alistConfig
|
||||||
|
|
||||||
func getToken() (string, error) {
|
a.baseURL = alistConfig.URL
|
||||||
resp, err := reqClient.R().SetBodyJsonMarshal(loginReq).Post("/api/auth/login")
|
a.client = getHttpClient()
|
||||||
if err != nil {
|
if alistConfig.Token != "" {
|
||||||
return "", err
|
a.token = alistConfig.Token
|
||||||
}
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||||
var loginResp loginResponse
|
defer cancel()
|
||||||
if err := json.Unmarshal(resp.Bytes(), &loginResp); err != nil {
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"/api/me", nil)
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if loginResp.Code != http.StatusOK {
|
|
||||||
return "", fmt.Errorf("%w: %s", ErrAlistLoginFailed, loginResp.Message)
|
|
||||||
}
|
|
||||||
return loginResp.Data.Token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshToken(client *req.Client) {
|
|
||||||
for {
|
|
||||||
time.Sleep(time.Duration(config.Cfg.Storage.Alist.TokenExp) * time.Second)
|
|
||||||
token, err := getToken()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to refresh jwt token: %v", err)
|
logger.L.Fatalf("Failed to create request: %v", err)
|
||||||
continue
|
return err
|
||||||
}
|
}
|
||||||
client.SetCommonHeader("Authorization", token)
|
req.Header.Set("Authorization", a.token)
|
||||||
logger.L.Info("Refreshed Alist jwt token")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Alist) Init() {
|
resp, err := a.client.Do(req)
|
||||||
basePath = config.Cfg.Storage.Alist.BasePath
|
if err != nil {
|
||||||
baseUrl = config.Cfg.Storage.Alist.URL
|
logger.L.Fatalf("Failed to send request: %v", err)
|
||||||
reqClient = req.C().SetTLSHandshakeTimeout(time.Second * 10).SetBaseURL(baseUrl).SetTimeout(time.Hour * 24)
|
return err
|
||||||
loginReq = &loginRequset{
|
}
|
||||||
Username: config.Cfg.Storage.Alist.Username,
|
defer resp.Body.Close()
|
||||||
Password: config.Cfg.Storage.Alist.Password,
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
logger.L.Fatalf("Failed to get alist user info: %s", resp.Status)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Fatalf("Failed to read response body: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var meResp meResponse
|
||||||
|
if err := json.Unmarshal(body, &meResp); err != nil {
|
||||||
|
logger.L.Fatalf("Failed to unmarshal me response: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if meResp.Code != http.StatusOK {
|
||||||
|
logger.L.Fatalf("Failed to get alist user info: %s", meResp.Message)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.L.Debugf("Logged in Alist as %s", meResp.Data.Username)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
token, err := getToken()
|
a.loginInfo = &loginRequest{
|
||||||
if err != nil {
|
Username: alistConfig.Username,
|
||||||
|
Password: alistConfig.Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.getToken(); err != nil {
|
||||||
logger.L.Fatalf("Failed to login to Alist: %v", err)
|
logger.L.Fatalf("Failed to login to Alist: %v", err)
|
||||||
os.Exit(1)
|
return err
|
||||||
}
|
}
|
||||||
logger.L.Debug("Logged in to Alist")
|
logger.L.Debug("Logged in to Alist")
|
||||||
reqClient.SetCommonHeader("Authorization", token)
|
|
||||||
go refreshToken(reqClient)
|
go a.refreshToken(*alistConfig)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type putResponse struct {
|
func (a *Alist) Type() types.StorageType {
|
||||||
Code int `json:"code"`
|
return types.StorageTypeAlist
|
||||||
Message string `json:"message"`
|
}
|
||||||
Data struct {
|
|
||||||
Task struct {
|
func (a *Alist) Name() string {
|
||||||
ID string `json:"id"`
|
return a.config.Name
|
||||||
Name string `json:"name"`
|
|
||||||
State int `json:"state"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Progress int `json:"progress"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
} `json:"task"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
|
func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
|
||||||
storagePath = path.Join(basePath, storagePath)
|
|
||||||
file, err := os.Open(filePath)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to open file: %w", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
fileBytes, err := io.ReadAll(file)
|
|
||||||
|
filestat, err := file.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to get file stats: %w", err)
|
||||||
}
|
}
|
||||||
resp, err := reqClient.R().
|
|
||||||
SetContext(ctx).
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, a.baseURL+"/api/fs/put", file)
|
||||||
SetBodyBytes(fileBytes).
|
|
||||||
SetHeaders(map[string]string{
|
|
||||||
"File-Path": url.QueryEscape(storagePath),
|
|
||||||
"As-Task": "true",
|
|
||||||
}).Put("/api/fs/put")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
|
req.Header.Set("Authorization", a.token)
|
||||||
|
req.Header.Set("File-Path", url.PathEscape(storagePath))
|
||||||
|
req.Header.Set("As-Task", "true")
|
||||||
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
|
req.ContentLength = filestat.Size()
|
||||||
|
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("failed to save file to Alist: %s", resp.Status)
|
return fmt.Errorf("failed to save file to Alist: %s", resp.Status)
|
||||||
}
|
}
|
||||||
var putResp putResponse
|
|
||||||
if err := json.Unmarshal(resp.Bytes(), &putResp); err != nil {
|
body, err := io.ReadAll(resp.Body)
|
||||||
return fmt.Errorf("failed to unmarshal put response: %v", err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read response body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var putResp putResponse
|
||||||
|
if err := json.Unmarshal(body, &putResp); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal put response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if putResp.Code != http.StatusOK {
|
if putResp.Code != http.StatusOK {
|
||||||
return fmt.Errorf("failed to save file to Alist: %d, %s", putResp.Code, putResp.Message)
|
return fmt.Errorf("failed to save file to Alist: %d, %s", putResp.Code, putResp.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Alist) JoinStoragePath(task types.Task) string {
|
||||||
|
return path.Join(a.config.BasePath, task.StoragePath)
|
||||||
|
}
|
||||||
|
|||||||
65
storage/alist/token.go
Normal file
65
storage/alist/token.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package alist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *Alist) getToken() error {
|
||||||
|
loginBody, err := json.Marshal(a.loginInfo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal login request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, a.baseURL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create login request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send login request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read login response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginResp loginResponse
|
||||||
|
if err := json.Unmarshal(body, &loginResp); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal login response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginResp.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("%w: %s", ErrAlistLoginFailed, loginResp.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.token = loginResp.Data.Token
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Alist) refreshToken(cfg config.AlistStorageConfig) {
|
||||||
|
tokenExp := cfg.TokenExp
|
||||||
|
if tokenExp <= 0 {
|
||||||
|
logger.L.Warn("Invalid token expiration time, using default value")
|
||||||
|
tokenExp = 3600
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Duration(tokenExp) * time.Second)
|
||||||
|
if err := a.getToken(); err != nil {
|
||||||
|
logger.L.Errorf("Failed to refresh jwt token: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.L.Info("Refreshed Alist jwt token")
|
||||||
|
}
|
||||||
|
}
|
||||||
44
storage/alist/types.go
Normal file
44
storage/alist/types.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package alist
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAlistLoginFailed = errors.New("failed to login to Alist")
|
||||||
|
)
|
||||||
|
|
||||||
|
type loginRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type loginResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
} `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"`
|
||||||
|
Data struct {
|
||||||
|
Task struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
State int `json:"state"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Progress int `json:"progress"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
} `json:"task"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
23
storage/alist/utils.go
Normal file
23
storage/alist/utils.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package alist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
httpClient *http.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
func getHttpClient() *http.Client {
|
||||||
|
if httpClient != nil {
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
|
httpClient = &http.Client{
|
||||||
|
Timeout: 12 * time.Hour,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
9
storage/errs.go
Normal file
9
storage/errs.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrStorageNameEmpty = errors.New("storage name is empty")
|
||||||
|
)
|
||||||
@@ -2,24 +2,54 @@ package local
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/duke-git/lancet/v2/fileutil"
|
"github.com/duke-git/lancet/v2/fileutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Local struct{}
|
type Local struct {
|
||||||
|
config config.LocalStorageConfig
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Local) Init() {
|
func (l *Local) Init(cfg config.StorageConfig) error {
|
||||||
err := os.MkdirAll(config.Cfg.Storage.Local.BasePath, os.ModePerm)
|
localConfig, ok := cfg.(*config.LocalStorageConfig)
|
||||||
if err != nil {
|
if !ok {
|
||||||
logger.L.Fatalf("Failed to create local storage directory: %s", err)
|
return fmt.Errorf("failed to cast local config")
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
if err := localConfig.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
l.config = *localConfig
|
||||||
|
err := os.MkdirAll(localConfig.BasePath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create local storage directory: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Local) Type() types.StorageType {
|
||||||
|
return types.StorageTypeLocal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Local) Name() string {
|
||||||
|
return l.config.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Local) Save(ctx context.Context, filePath, storagePath string) error {
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Local) JoinStoragePath(task types.Task) string {
|
||||||
|
return filepath.Join(l.config.BasePath, task.StoragePath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"fmt"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
"github.com/krau/SaveAny-Bot/storage/alist"
|
"github.com/krau/SaveAny-Bot/storage/alist"
|
||||||
@@ -15,58 +13,92 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
Init()
|
Init(cfg config.StorageConfig) error
|
||||||
Save(cttx context.Context, filePath, storagePath string) error
|
Type() types.StorageType
|
||||||
|
Name() string
|
||||||
|
JoinStoragePath(task types.Task) string
|
||||||
|
Save(cttx context.Context, localFilePath, storagePath string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var Storages = make(map[types.StorageType]Storage)
|
var Storages = make(map[string]Storage)
|
||||||
var StorageKeys = make([]types.StorageType, 0)
|
|
||||||
|
|
||||||
func Init() {
|
// GetStorageByName returns storage by name from cache or creates new one
|
||||||
logger.L.Debug("Initializing storage...")
|
func GetStorageByName(name string) (Storage, error) {
|
||||||
if config.Cfg.Storage.Alist.Enable {
|
if name == "" {
|
||||||
Storages[types.Alist] = new(alist.Alist)
|
return nil, ErrStorageNameEmpty
|
||||||
Storages[types.Alist].Init()
|
|
||||||
}
|
|
||||||
if config.Cfg.Storage.Local.Enable {
|
|
||||||
Storages[types.Local] = new(local.Local)
|
|
||||||
Storages[types.Local].Init()
|
|
||||||
}
|
|
||||||
if config.Cfg.Storage.Webdav.Enable {
|
|
||||||
Storages[types.Webdav] = new(webdav.Webdav)
|
|
||||||
Storages[types.Webdav].Init()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range Storages {
|
storage, ok := Storages[name]
|
||||||
StorageKeys = append(StorageKeys, k)
|
if ok {
|
||||||
|
return storage, nil
|
||||||
|
}
|
||||||
|
cfg := config.Cfg.GetStorageByName(name)
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("未找到存储 %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
slice.Sort(StorageKeys)
|
storage, err := NewStorage(cfg)
|
||||||
|
if err != nil {
|
||||||
logger.L.Debug("Storage initialized")
|
return nil, err
|
||||||
|
}
|
||||||
|
Storages[name] = storage
|
||||||
|
return storage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Save(storageType types.StorageType, ctx context.Context, filePath, storagePath string) error {
|
// 检查 user 是否可用指定的 storage, 若不可用则返回未找到错误
|
||||||
if ctx == nil {
|
func GetStorageByUserIDAndName(chatID int64, name string) (Storage, error) {
|
||||||
ctx = context.Background()
|
if name == "" {
|
||||||
|
return nil, ErrStorageNameEmpty
|
||||||
}
|
}
|
||||||
if storageType != types.StorageAll {
|
|
||||||
return Storages[storageType].Save(ctx, filePath, storagePath)
|
if !config.Cfg.HasStorage(chatID, name) {
|
||||||
|
return nil, fmt.Errorf("没有找到用户 %d 的存储 %s", chatID, name)
|
||||||
}
|
}
|
||||||
errs := make([]error, 0)
|
|
||||||
var wg sync.WaitGroup
|
return GetStorageByName(name)
|
||||||
for _, storage := range Storages {
|
}
|
||||||
wg.Add(1)
|
|
||||||
go func(storage Storage) {
|
func GetUserStorages(chatID int64) []Storage {
|
||||||
defer wg.Done()
|
var storages []Storage
|
||||||
if err := storage.Save(ctx, filePath, storagePath); err != nil {
|
for _, name := range config.Cfg.GetStorageNamesByUserID(chatID) {
|
||||||
errs = append(errs, err)
|
storage, err := GetStorageByName(name)
|
||||||
}
|
if err != nil {
|
||||||
}(storage)
|
continue
|
||||||
}
|
}
|
||||||
wg.Wait()
|
storages = append(storages, storage)
|
||||||
if len(errs) > 0 {
|
}
|
||||||
return errors.Join(errs...)
|
return storages
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
type StorageConstructor func() Storage
|
||||||
|
|
||||||
|
var storageConstructors = map[string]StorageConstructor{
|
||||||
|
string(types.StorageTypeAlist): func() Storage { return new(alist.Alist) },
|
||||||
|
string(types.StorageTypeLocal): func() Storage { return new(local.Local) },
|
||||||
|
string(types.StorageTypeWebdav): func() Storage { return new(webdav.Webdav) },
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorage(cfg config.StorageConfig) (Storage, error) {
|
||||||
|
constructor, ok := storageConstructors[string(cfg.GetType())]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("不支持的存储类型: %s", cfg.GetType())
|
||||||
|
}
|
||||||
|
|
||||||
|
storage := constructor()
|
||||||
|
if err := storage.Init(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("初始化 %s 存储失败: %w", cfg.GetName(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadStorages() {
|
||||||
|
logger.L.Info("加载存储...")
|
||||||
|
for _, storage := range config.Cfg.Storages {
|
||||||
|
_, err := GetStorageByName(storage.GetName())
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("加载存储 %s 失败: %v", storage.GetName(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.L.Infof("成功加载 %d 个存储", len(Storages))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,39 +2,51 @@ package webdav
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
"github.com/studio-b12/gowebdav"
|
"github.com/studio-b12/gowebdav"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Webdav struct{}
|
type Webdav struct {
|
||||||
|
config config.WebdavStorageConfig
|
||||||
|
client *gowebdav.Client
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
func (w *Webdav) Init(cfg config.StorageConfig) error {
|
||||||
Client *gowebdav.Client
|
webdavConfig, ok := cfg.(*config.WebdavStorageConfig)
|
||||||
basePath string
|
if !ok {
|
||||||
)
|
return fmt.Errorf("failed to cast webdav config")
|
||||||
|
|
||||||
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)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
Client.SetTimeout(24 * time.Hour)
|
if err := webdavConfig.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.config = *webdavConfig
|
||||||
|
client := gowebdav.NewClient(webdavConfig.URL, webdavConfig.Username, webdavConfig.Password)
|
||||||
|
if err := client.Connect(); err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to webdav server: %w", err)
|
||||||
|
}
|
||||||
|
client.SetTimeout(12 * time.Hour)
|
||||||
|
w.client = client
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webdav) Type() types.StorageType {
|
||||||
|
return types.StorageTypeWebdav
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webdav) Name() string {
|
||||||
|
return w.config.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error {
|
func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error {
|
||||||
storagePath = path.Join(basePath, storagePath)
|
if err := w.client.MkdirAll(path.Dir(storagePath), os.ModePerm); err != nil {
|
||||||
if err := Client.MkdirAll(filepath.Dir(storagePath), os.ModePerm); err != nil {
|
logger.L.Errorf("Failed to create directory %s: %v", path.Dir(storagePath), err)
|
||||||
logger.L.Errorf("Failed to create directory %s: %v", filepath.Dir(storagePath), err)
|
|
||||||
return ErrFailedToCreateDirectory
|
return ErrFailedToCreateDirectory
|
||||||
}
|
}
|
||||||
file, err := os.Open(filePath)
|
file, err := os.Open(filePath)
|
||||||
@@ -44,9 +56,13 @@ func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if err := Client.WriteStream(storagePath, file, os.ModePerm); err != nil {
|
if err := w.client.WriteStream(storagePath, file, os.ModePerm); err != nil {
|
||||||
logger.L.Errorf("Failed to write file %s: %v", storagePath, err)
|
logger.L.Errorf("Failed to write file %s: %v", storagePath, err)
|
||||||
return ErrFailedToWriteFile
|
return ErrFailedToWriteFile
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Webdav) JoinStoragePath(task types.Task) string {
|
||||||
|
return path.Join(w.config.BasePath, task.StoragePath)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,16 +6,19 @@ import (
|
|||||||
|
|
||||||
type ReceivedFile struct {
|
type ReceivedFile struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Processing bool
|
Processing bool
|
||||||
ChatID int64
|
// Which chat the file is from
|
||||||
MessageID int
|
ChatID int64 `gorm:"uniqueIndex:idx_chat_id_message_id;not null"`
|
||||||
|
// Which message the file is from
|
||||||
|
MessageID int `gorm:"uniqueIndex:idx_chat_id_message_id;not null"`
|
||||||
ReplyMessageID int
|
ReplyMessageID int
|
||||||
|
ReplyChatID int64
|
||||||
FileName string
|
FileName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
UserID int64 `gorm:"uniqueIndex"`
|
ChatID int64 `gorm:"uniqueIndex;not null"`
|
||||||
Silent bool
|
Silent bool
|
||||||
DefaultStorage string
|
DefaultStorage string // Default storage name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
)
|
)
|
||||||
@@ -19,29 +22,38 @@ var (
|
|||||||
type StorageType string
|
type StorageType string
|
||||||
|
|
||||||
var (
|
var (
|
||||||
StorageAll StorageType = "all"
|
StorageTypeLocal StorageType = "local"
|
||||||
Local StorageType = "local"
|
StorageTypeWebdav StorageType = "webdav"
|
||||||
Webdav StorageType = "webdav"
|
StorageTypeAlist StorageType = "alist"
|
||||||
Alist StorageType = "alist"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var StorageTypes = []StorageType{Local, Alist, Webdav, StorageAll}
|
var StorageTypes = []StorageType{StorageTypeLocal, StorageTypeAlist, StorageTypeWebdav}
|
||||||
|
var StorageTypeDisplay = map[StorageType]string{
|
||||||
|
StorageTypeLocal: "本地磁盘",
|
||||||
|
StorageTypeWebdav: "WebDAV",
|
||||||
|
StorageTypeAlist: "Alist",
|
||||||
|
}
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
Error error
|
Error error
|
||||||
Status TaskStatus
|
Status TaskStatus
|
||||||
File *File
|
File *File
|
||||||
Storage StorageType
|
StorageName string
|
||||||
StoragePath string
|
StoragePath string
|
||||||
|
StartTime time.Time
|
||||||
|
|
||||||
MessageID int
|
FileMessageID int
|
||||||
ChatID int64
|
FileChatID int64
|
||||||
|
// to track the reply message
|
||||||
ReplyMessageID int
|
ReplyMessageID int
|
||||||
|
ReplyChatID int64
|
||||||
|
// to track the user
|
||||||
|
UserID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Task) String() string {
|
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 {
|
func (t Task) FileName() string {
|
||||||
@@ -53,3 +65,18 @@ type File struct {
|
|||||||
FileSize int64
|
FileSize int64
|
||||||
FileName string
|
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