Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
488d709d85 | ||
|
|
66454b082a | ||
|
|
70e83e62d9 | ||
|
|
d2ddb9193a | ||
|
|
5f78db90c7 | ||
|
|
c3a4702e79 | ||
|
|
e731cfee9e | ||
|
|
75de86fe97 | ||
|
|
6d4e97b4bb | ||
|
|
239d5ad562 | ||
|
|
e76f191922 | ||
|
|
a619ed2f22 | ||
|
|
838dfc35a1 | ||
|
|
6ecee6d561 | ||
|
|
c1827f93a9 | ||
|
|
aaf3f7c35f | ||
|
|
02fbea4eb0 | ||
|
|
bf9aef6eb7 | ||
|
|
230c07fd55 | ||
|
|
18de349dc3 | ||
|
|
cef1a5c194 | ||
|
|
99f8f0cb27 | ||
|
|
789c14134c | ||
|
|
5bb3b595aa | ||
|
|
609289c16a | ||
|
|
c8c348a182 | ||
|
|
725acd0199 | ||
|
|
166c27c70f | ||
|
|
3bdef20e85 | ||
|
|
50fba3f910 | ||
|
|
87d3f14392 | ||
|
|
30452c8d46 | ||
|
|
300f7723af | ||
|
|
491ba55f1e | ||
|
|
32519b8c08 | ||
|
|
7ffd9891a0 | ||
|
|
347a60f1f7 | ||
|
|
da69fe1354 | ||
|
|
746ca026ba | ||
|
|
a8c64675e5 | ||
|
|
3918f6eee2 | ||
|
|
8d44b43c82 | ||
|
|
f14c4367f8 | ||
|
|
3e3a320672 | ||
|
|
19efab0665 | ||
|
|
635f00ac71 | ||
|
|
2d2becccf6 | ||
|
|
ed0837a89b | ||
|
|
65fee89e14 | ||
|
|
8e180006f0 | ||
|
|
721c9666eb | ||
|
|
6f35401181 | ||
|
|
72ae2ce079 | ||
|
|
495ad3ea5c | ||
|
|
3def9df4b4 | ||
|
|
790a32d297 | ||
|
|
f7779224ef | ||
|
|
7d899ae088 | ||
|
|
7e67bdb7e2 | ||
|
|
0071780ff4 | ||
|
|
0a95431468 | ||
|
|
34525c5b11 | ||
|
|
6ac6d79fb6 | ||
|
|
f21a82ad43 | ||
|
|
73f6647f8d | ||
|
|
6fbb4609f9 | ||
|
|
802c908384 | ||
|
|
5d403056d0 | ||
|
|
8e2dd37155 | ||
|
|
9c7ed833fd | ||
|
|
f9d601bd8a | ||
|
|
152f473131 | ||
|
|
7015081a84 | ||
|
|
be6444cf96 | ||
|
|
98ba7c50e7 | ||
|
|
0c31d908cc | ||
|
|
9e776b22fb | ||
|
|
d6f8603656 | ||
|
|
9c42bee662 | ||
|
|
b96340dd46 | ||
|
|
a5ba01e219 | ||
|
|
d00e907735 | ||
|
|
418f9bd2bc | ||
|
|
28b4585dba | ||
|
|
d2669f0c99 | ||
|
|
c9921926e3 | ||
|
|
d7cd2ede01 | ||
|
|
ed21b65c98 | ||
|
|
8975589c43 | ||
|
|
27dca2e343 | ||
|
|
5c8261c34a | ||
|
|
cbc2dc82d8 |
5
.github/FUNDING.yml
vendored
Normal file
5
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
custom: [
|
||||||
|
"https://afdian.com/a/acherkrau"
|
||||||
|
]
|
||||||
35
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: "👾 报告 bug"
|
||||||
|
description: "报告 bug"
|
||||||
|
labels:
|
||||||
|
- ":space_invader: Bug"
|
||||||
|
assignees:
|
||||||
|
- krau
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "👾 问题描述"
|
||||||
|
description: "What happened?"
|
||||||
|
placeholder: "When called ... happens ..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "⚡️ 预期行为"
|
||||||
|
description: "What was expected?"
|
||||||
|
placeholder: "It should be ..."
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "📄 配置文件"
|
||||||
|
description: "Please provide your config file"
|
||||||
|
placeholder: "请自行隐去密钥信息"
|
||||||
|
render: toml
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "🔍 日志"
|
||||||
|
description: "Please provide logs"
|
||||||
|
placeholder: "可删除隐私信息"
|
||||||
|
render: shell
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Thank you for contributing to the project :slightly_smiling_face:
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: 💬 不知道如何正确使用?
|
||||||
|
url: https://github.com/krau/SaveAny-Bot/discussions
|
||||||
|
about: "前往讨论区提问"
|
||||||
|
- name: 📄 文档
|
||||||
|
url: https://sabot.unv.app
|
||||||
|
about: "查看文档"
|
||||||
33
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: "⭐️ 功能请求"
|
||||||
|
description: "功能请求"
|
||||||
|
labels:
|
||||||
|
- ":fire: Enhancement"
|
||||||
|
assignees:
|
||||||
|
- krau
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# 请详细描述你想要的功能
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "⭐️ Feature description"
|
||||||
|
description: "What new feature you want to see?"
|
||||||
|
placeholder: "Add ... in order to ..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "🌈 Your view"
|
||||||
|
description: "How do you see this feature will be used and/or implemented?"
|
||||||
|
placeholder: "It should be like ..."
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: "🧐 Code example"
|
||||||
|
description: "You can provide code (or pseudocode) example"
|
||||||
|
placeholder: "Cool code that will work ..."
|
||||||
|
render: Go
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Thank you for contributing to the project :slightly_smiling_face:
|
||||||
5
.github/workflows/build-release.yml
vendored
5
.github/workflows/build-release.yml
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
name: Build Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
@@ -36,6 +38,9 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
goos: [linux, darwin, windows]
|
goos: [linux, darwin, windows]
|
||||||
goarch: [amd64, arm64]
|
goarch: [amd64, arm64]
|
||||||
|
exclude:
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm64
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
22
.github/workflows/docs.yml
vendored
Normal file
22
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Deploy Docs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "docs/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
key: ${{ github.ref }}
|
||||||
|
path: .cache
|
||||||
|
- run: pip install mkdocs-material
|
||||||
|
- run: cd docs && mkdocs gh-deploy --force
|
||||||
17
.github/workflows/update-contributors.yml
vendored
Normal file
17
.github/workflows/update-contributors.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: Update Contributors
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
contrib-readme-job:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: A job to automate contrib in readme
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Contribute List
|
||||||
|
uses: akhilmhdh/contributors-readme-action@v2.3.10
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,4 +5,5 @@ data/
|
|||||||
downloads/
|
downloads/
|
||||||
cache/
|
cache/
|
||||||
session.*
|
session.*
|
||||||
cache.db
|
cache.db
|
||||||
|
.vscode/
|
||||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
// 使用 IntelliSense 了解相关属性。
|
|
||||||
// 悬停以查看现有属性的描述。
|
|
||||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Launch Package",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"mode": "auto",
|
|
||||||
"program": "main.go",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
11
Dockerfile
11
Dockerfile
@@ -11,8 +11,17 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o saveany-bot .
|
|||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN addgroup -S saveany && adduser -S saveany -G saveany
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN mkdir -p /app/data /app/downloads /app/cache && \
|
||||||
|
chown -R saveany:saveany /app
|
||||||
|
|
||||||
COPY --from=builder /app/saveany-bot .
|
COPY --from=builder /app/saveany-bot .
|
||||||
|
|
||||||
CMD ["./saveany-bot"]
|
RUN chmod +x /app/saveany-bot
|
||||||
|
|
||||||
|
USER saveany
|
||||||
|
|
||||||
|
CMD ["./saveany-bot"]
|
||||||
|
|||||||
137
README.md
137
README.md
@@ -2,104 +2,67 @@
|
|||||||
|
|
||||||
# <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)
|
**简体中文** | [English](README_EN.md)
|
||||||
|
|
||||||
把 Telegram 的文件保存到各类存储端.
|
把 Telegram 的文件保存到各类存储端.
|
||||||
|
|
||||||
> _就像 PikPak Bot 一样_
|
> _就像 PikPak Bot 一样_
|
||||||
|
|
||||||
</div
|
|
||||||
|
|
||||||
Demo Video:
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
[SaveAny-Bot 演示视频 | The Demo of SaveAny-Bot.webm](https://github.com/user-attachments/assets/a0de2453-a4d1-4a12-81fb-9d84856dce09)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 部署
|
## [部署](https://sabot.unv.app/deploy/)
|
||||||
|
|
||||||
### 从二进制文件部署
|
## [参与开发](https://sabot.unv.app/contribute/)
|
||||||
|
|
||||||
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
|
|
||||||
|
|
||||||
在解压后目录新建 `config.toml` 文件, 参考 [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
|
||||||
|
|
||||||
运行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x saveany-bot
|
|
||||||
./saveany-bot
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 添加为 systemd 服务
|
|
||||||
|
|
||||||
创建文件 `/etc/systemd/system/saveany-bot.service` 并写入以下内容:
|
|
||||||
|
|
||||||
```
|
|
||||||
[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
|
|
||||||
```
|
|
||||||
|
|
||||||
设为开机启动并启动服务:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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` 升级到最新版
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./saveany-bot upgrade
|
|
||||||
```
|
|
||||||
|
|
||||||
如果是 Docker 部署, 使用以下命令更新:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/krau/saveany-bot:latest
|
|
||||||
docker restart saveany-bot
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用
|
|
||||||
|
|
||||||
向 Bot 发送(转发)文件, 或发送公开频道的消息链接, 按照提示操作.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 赞助
|
||||||
|
|
||||||
|
本项目受到 [YxVM](https://yxvm.com/) 与 [NodeSupport](https://github.com/NodeSeekDev/NodeSupport) 的支持.
|
||||||
|
|
||||||
|
如果这个项目对你有帮助, 你可以考虑通过以下方式赞助我:
|
||||||
|
|
||||||
|
- [爱发电](https://afdian.com/a/acherkrau)
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
<!-- readme: contributors -start -->
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/krau">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/71133316?v=4" width="100;" alt="krau"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Krau</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/TG-Twilight">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/121682528?v=4" width="100;" alt="TG-Twilight"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Simon Twilight</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/ysicing">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="100;" alt="ysicing"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>缘生</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/ahcorn">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/42889600?v=4" width="100;" alt="ahcorn"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>安和</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tbody>
|
||||||
|
</table>
|
||||||
|
<!-- readme: contributors -end -->
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
||||||
- [gotd](https://github.com/gotd/td)
|
- [gotd](https://github.com/gotd/td)
|
||||||
|
|||||||
20
README_EN.md
20
README_EN.md
@@ -10,21 +10,13 @@ Save Telegram files to various storage endpoints.
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Demo Video:
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
[SaveAny-Bot Demo Video.webm](https://github.com/user-attachments/assets/a0de2453-a4d1-4a12-81fb-9d84856dce09)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Deploy from Binary
|
### Deploy from Binary
|
||||||
|
|
||||||
Download the binary file for your platform from the [Release](https://github.com/krau/SaveAny-Bot/releases) page.
|
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.
|
Create a `config.toml` file in the extracted directory, refer to [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) for configuration.
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -62,7 +54,7 @@ systemctl enable --now saveany-bot
|
|||||||
|
|
||||||
#### Docker Compose
|
#### 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.
|
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.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) for configuration.
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -100,6 +92,14 @@ Send (forward) files to the Bot and follow the prompts.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
This project is supported by [YxVM](https://yxvm.com/) and [NodeSupport](https://github.com/NodeSeekDev/NodeSupport).
|
||||||
|
|
||||||
|
You can consider sponsoring me if this project helps you:
|
||||||
|
|
||||||
|
- [Afdian](https://afdian.com/a/acherkrau)
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
|
||||||
- [gotd](https://github.com/gotd/td)
|
- [gotd](https://github.com/gotd/td)
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package bootstrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/bot"
|
|
||||||
"github.com/krau/SaveAny-Bot/common"
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
|
||||||
"github.com/krau/SaveAny-Bot/dao"
|
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
func InitAll() {
|
|
||||||
if err := config.Init(); err != nil {
|
|
||||||
fmt.Println("加载配置文件失败: ", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
logger.InitLogger()
|
|
||||||
logger.L.Info("正在启动 SaveAny-Bot...")
|
|
||||||
storage.LoadStorages()
|
|
||||||
common.Init()
|
|
||||||
dao.Init()
|
|
||||||
bot.Init()
|
|
||||||
}
|
|
||||||
18
bot/bot.go
18
bot/bot.go
@@ -11,8 +11,8 @@ import (
|
|||||||
"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/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,9 +27,10 @@ func newProxyDialer(proxyUrl string) (proxy.Dialer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
logger.L.Info("初始化 Telegram 客户端...")
|
common.Log.Info("初始化 Telegram 客户端...")
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Cfg.Telegram.Timeout)*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
go InitTelegraphClient()
|
||||||
resultChan := make(chan struct {
|
resultChan := make(chan struct {
|
||||||
client *gotgproto.Client
|
client *gotgproto.Client
|
||||||
err error
|
err error
|
||||||
@@ -55,10 +56,11 @@ func Init() {
|
|||||||
config.Cfg.Telegram.AppHash,
|
config.Cfg.Telegram.AppHash,
|
||||||
gotgproto.ClientTypeBot(config.Cfg.Telegram.Token),
|
gotgproto.ClientTypeBot(config.Cfg.Telegram.Token),
|
||||||
&gotgproto.ClientOpts{
|
&gotgproto.ClientOpts{
|
||||||
Session: sessionMaker.SqlSession(sqlite.Open("data/session.db")),
|
Session: sessionMaker.SqlSession(sqlite.Open(config.Cfg.DB.Session)),
|
||||||
DisableCopyright: true,
|
DisableCopyright: true,
|
||||||
Middlewares: FloodWaitMiddleware(),
|
Middlewares: FloodWaitMiddleware(),
|
||||||
Resolver: resolver,
|
Resolver: resolver,
|
||||||
|
MaxRetries: config.Cfg.Telegram.RpcRetry,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -76,6 +78,8 @@ func Init() {
|
|||||||
{Command: "silent", Description: "开启/关闭静默模式"},
|
{Command: "silent", Description: "开启/关闭静默模式"},
|
||||||
{Command: "storage", Description: "设置默认存储端"},
|
{Command: "storage", Description: "设置默认存储端"},
|
||||||
{Command: "save", Description: "保存所回复的文件"},
|
{Command: "save", Description: "保存所回复的文件"},
|
||||||
|
{Command: "dir", Description: "管理存储文件夹"},
|
||||||
|
{Command: "rule", Description: "管理规则"},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
resultChan <- struct {
|
resultChan <- struct {
|
||||||
@@ -86,15 +90,15 @@ func Init() {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
logger.L.Fatal("初始化客户端失败: 超时")
|
common.Log.Fatal("初始化客户端失败: 超时")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
if result.err != nil {
|
if result.err != nil {
|
||||||
logger.L.Fatalf("初始化客户端失败: %s", result.err)
|
common.Log.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("客户端初始化完成")
|
common.Log.Info("客户端初始化完成")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
207
bot/handle_add_task.go
Normal file
207
bot/handle_add_task.go
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
|
"github.com/krau/SaveAny-Bot/queue"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
// TODO: 回调数据用户独立鉴权 (处理 bot 在群聊中的情况)
|
||||||
|
if !slice.Contain(config.Cfg.GetUsersID(), update.CallbackQuery.UserID) {
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "你没有权限",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
args := strings.Split(string(update.CallbackQuery.Data), " ")
|
||||||
|
addToDir := args[0] == "add_to_dir" // 已经选择了路径
|
||||||
|
cbDataId, _ := strconv.Atoi(args[1])
|
||||||
|
cbData, err := dao.GetCallbackData(uint(cbDataId))
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取回调数据失败: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "获取回调数据失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
data := strings.Split(cbData, " ")
|
||||||
|
fileChatID, _ := strconv.Atoi(data[0])
|
||||||
|
fileMessageID, _ := strconv.Atoi(data[1])
|
||||||
|
storageName := data[2]
|
||||||
|
dirIdInt, _ := strconv.Atoi(data[3])
|
||||||
|
dirId := uint(dirIdInt)
|
||||||
|
|
||||||
|
user, err := dao.GetUserByChatID(update.CallbackQuery.UserID)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取用户失败: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "获取用户失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if !addToDir {
|
||||||
|
dirs, err := dao.GetDirsByUserIDAndStorageName(user.ID, storageName)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
common.Log.Errorf("获取路径失败: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "获取路径失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if len(dirs) != 0 {
|
||||||
|
markup, err := getSelectDirMarkup(fileChatID, fileMessageID, storageName, dirs)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取路径失败: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "获取路径失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
|
Message: "请选择要保存到的路径",
|
||||||
|
ReplyMarkup: markup,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("编辑消息失败: %s", err)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
common.Log.Tracef("Got add to queue: chatID: %d, messageID: %d, storage: %s", fileChatID, fileMessageID, storageName)
|
||||||
|
record, err := dao.GetReceivedFileByChatAndMessageID(int64(fileChatID), fileMessageID)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取记录失败: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "查询记录失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if update.CallbackQuery.MsgID != record.ReplyMessageID {
|
||||||
|
record.ReplyMessageID = update.CallbackQuery.MsgID
|
||||||
|
if err := dao.SaveReceivedFile(record); err != nil {
|
||||||
|
common.Log.Errorf("更新接收的文件失败: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dir *dao.Dir
|
||||||
|
if addToDir && dirId != 0 {
|
||||||
|
dir, err = dao.GetDirByID(dirId)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取路径失败: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "获取路径失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var task types.Task
|
||||||
|
if record.IsTelegraph {
|
||||||
|
task = types.Task{
|
||||||
|
Ctx: ctx,
|
||||||
|
Status: types.Pending,
|
||||||
|
IsTelegraph: true,
|
||||||
|
TelegraphURL: record.TelegraphURL,
|
||||||
|
StorageName: storageName,
|
||||||
|
FileChatID: record.ChatID,
|
||||||
|
FileMessageID: record.MessageID,
|
||||||
|
ReplyMessageID: record.ReplyMessageID,
|
||||||
|
ReplyChatID: record.ReplyChatID,
|
||||||
|
UserID: update.GetUserChat().GetID(),
|
||||||
|
}
|
||||||
|
if dir != nil {
|
||||||
|
task.StoragePath = path.Join(dir.Path, record.FileName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
file, err := FileFromMessage(ctx, record.ChatID, record.MessageID, record.FileName)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取消息中的文件失败: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: fmt.Sprintf("获取消息中的文件失败: %s", err),
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
task = types.Task{
|
||||||
|
Ctx: ctx,
|
||||||
|
Status: types.Pending,
|
||||||
|
File: file,
|
||||||
|
StorageName: storageName,
|
||||||
|
FileChatID: record.ChatID,
|
||||||
|
ReplyMessageID: record.ReplyMessageID,
|
||||||
|
FileMessageID: record.MessageID,
|
||||||
|
ReplyChatID: record.ReplyChatID,
|
||||||
|
UserID: update.GetUserChat().GetID(),
|
||||||
|
}
|
||||||
|
if dir != nil {
|
||||||
|
task.StoragePath = path.Join(dir.Path, file.FileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.AddTask(&task)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
common.Log.Errorf("Failed to build entity: %s", err)
|
||||||
|
} else {
|
||||||
|
text, entities = entityBuilder.Complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: text,
|
||||||
|
Entities: entities,
|
||||||
|
ID: record.ReplyMessageID,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
27
bot/handle_cancel_task.go
Normal file
27
bot/handle_cancel_task.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/queue"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cancelTask(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
key := strings.Split(string(update.CallbackQuery.Data), " ")[1]
|
||||||
|
ok := queue.CancelTask(key)
|
||||||
|
if ok {
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Message: "任务已取消",
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Message: "任务取消失败",
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ func (c *ConversationState) SetData(key string, value interface{}) {
|
|||||||
// func handleConversationState(ctx *ext.Context, update *ext.Update, state *ConversationState) error {
|
// func handleConversationState(ctx *ext.Context, update *ext.Update, state *ConversationState) error {
|
||||||
// switch state.conversationType {
|
// switch state.conversationType {
|
||||||
// default:
|
// default:
|
||||||
// logger.L.Errorf("Unknown conversation type: %s", state.conversationType)
|
// common.Log.Errorf("Unknown conversation type: %s", state.conversationType)
|
||||||
// }
|
// }
|
||||||
// return dispatcher.EndGroups
|
// return dispatcher.EndGroups
|
||||||
// }
|
// }
|
||||||
110
bot/handle_dir.go
Normal file
110
bot/handle_dir.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sendDirHelp(ctx *ext.Context, update *ext.Update, userChatID int64) error {
|
||||||
|
dirs, err := dao.GetUserDirsByChatID(userChatID)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取用户路径失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取用户路径失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextStyledTextArray(
|
||||||
|
[]styling.StyledTextOption{
|
||||||
|
styling.Bold("使用方法: /dir <操作> <参数...>"),
|
||||||
|
styling.Plain("\n\n可用操作:\n"),
|
||||||
|
styling.Code("add"),
|
||||||
|
styling.Plain(" <存储名> <路径> - 添加路径\n"),
|
||||||
|
styling.Code("del"),
|
||||||
|
styling.Plain(" <路径ID> - 删除路径\n"),
|
||||||
|
styling.Plain("\n添加路径示例:\n"),
|
||||||
|
styling.Code("/dir add local1 path/to/dir"),
|
||||||
|
styling.Plain("\n\n删除路径示例:\n"),
|
||||||
|
styling.Code("/dir del 3"),
|
||||||
|
styling.Plain("\n\n当前已添加的路径:\n"),
|
||||||
|
styling.Blockquote(func() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, dir := range dirs {
|
||||||
|
sb.WriteString(fmt.Sprintf("%d: ", dir.ID))
|
||||||
|
sb.WriteString(dir.StorageName)
|
||||||
|
sb.WriteString(" - ")
|
||||||
|
sb.WriteString(dir.Path)
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}(), true),
|
||||||
|
},
|
||||||
|
), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func dirCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||||
|
if len(args) < 2 {
|
||||||
|
return sendDirHelp(ctx, update, update.GetUserChat().GetID())
|
||||||
|
}
|
||||||
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取用户失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
switch args[1] {
|
||||||
|
case "add":
|
||||||
|
// /dir add local1 path/to/dir
|
||||||
|
if len(args) < 4 {
|
||||||
|
return sendDirHelp(ctx, update, update.GetUserChat().GetID())
|
||||||
|
}
|
||||||
|
return addDir(ctx, update, user, args[2], args[3])
|
||||||
|
case "del":
|
||||||
|
// /dir del 3
|
||||||
|
if len(args) < 3 {
|
||||||
|
return sendDirHelp(ctx, update, update.GetUserChat().GetID())
|
||||||
|
}
|
||||||
|
dirID, err := strconv.Atoi(args[2])
|
||||||
|
if err != nil {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("路径ID无效"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
return delDir(ctx, update, user, dirID)
|
||||||
|
default:
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("未知操作"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addDir(ctx *ext.Context, update *ext.Update, user *dao.User, storageName, path string) error {
|
||||||
|
if _, err := storage.GetStorageByUserIDAndName(user.ChatID, storageName); err != nil {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(err.Error()), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dao.CreateDirForUser(user.ID, storageName, path); err != nil {
|
||||||
|
common.Log.Errorf("创建路径失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("创建路径失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("路径添加成功"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func delDir(ctx *ext.Context, update *ext.Update, user *dao.User, dirID int) error {
|
||||||
|
if err := dao.DeleteDirByID(uint(dirID)); err != nil {
|
||||||
|
common.Log.Errorf("删除路径失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("删除路径失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("路径删除成功"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
85
bot/handle_file.go
Normal file
85
bot/handle_file.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
common.Log.Trace("Got media: ", update.EffectiveMessage.Media.TypeName())
|
||||||
|
supported, err := supportedMediaFilter(update.EffectiveMessage.Message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !supported {
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
common.Log.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 := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("回复失败: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
media := update.EffectiveMessage.Media
|
||||||
|
file, err := FileFromMedia(media, "")
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取文件失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if file.FileName == "" {
|
||||||
|
file.FileName = GenFileNameFromMessage(*update.EffectiveMessage.Message, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dao.SaveReceivedFile(&dao.ReceivedFile{
|
||||||
|
Processing: false,
|
||||||
|
FileName: file.FileName,
|
||||||
|
ChatID: update.EffectiveChat().GetID(),
|
||||||
|
MessageID: update.EffectiveMessage.ID,
|
||||||
|
ReplyMessageID: msg.ID,
|
||||||
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
|
}); err != nil {
|
||||||
|
common.Log.Errorf("添加接收的文件失败: %s", err)
|
||||||
|
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("添加接收的文件失败: %s", err),
|
||||||
|
ID: msg.ID,
|
||||||
|
}); err != nil {
|
||||||
|
common.Log.Errorf("编辑消息失败: %s", err)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.Silent || user.DefaultStorage == "" {
|
||||||
|
return ProvideSelectMessage(ctx, update, file.FileName, update.EffectiveChat().GetID(), update.EffectiveMessage.ID, msg.ID)
|
||||||
|
}
|
||||||
|
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||||
|
Ctx: ctx,
|
||||||
|
Status: types.Pending,
|
||||||
|
File: file,
|
||||||
|
StorageName: user.DefaultStorage,
|
||||||
|
FileChatID: update.EffectiveChat().GetID(),
|
||||||
|
ReplyMessageID: msg.ID,
|
||||||
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
|
FileMessageID: update.EffectiveMessage.ID,
|
||||||
|
UserID: user.ChatID,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,8 +8,8 @@ import (
|
|||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/dao"
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
@@ -21,7 +20,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||||
logger.L.Trace("Got link message")
|
common.Log.Trace("Got link message")
|
||||||
link := linkRegex.FindString(update.EffectiveMessage.Text)
|
link := linkRegex.FindString(update.EffectiveMessage.Text)
|
||||||
if link == "" {
|
if link == "" {
|
||||||
return dispatcher.ContinueGroups
|
return dispatcher.ContinueGroups
|
||||||
@@ -30,27 +29,43 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if len(strSlice) < 3 {
|
if len(strSlice) < 3 {
|
||||||
return dispatcher.ContinueGroups
|
return dispatcher.ContinueGroups
|
||||||
}
|
}
|
||||||
messageID, err := strconv.Atoi(strSlice[2])
|
messageID, err := strconv.Atoi(strSlice[len(strSlice)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("解析消息 ID 失败: %s", err)
|
common.Log.Errorf("解析消息 ID 失败: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("无法解析消息 ID"), nil)
|
ctx.Reply(update, ext.ReplyTextString("无法解析消息 ID"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
chatUsername := strSlice[1]
|
var linkChatID int64
|
||||||
linkChat, err := ctx.ResolveUsername(chatUsername)
|
if len(strSlice) == 3 {
|
||||||
if err != nil {
|
chatUsername := strSlice[1]
|
||||||
logger.L.Errorf("解析 Chat ID 失败: %s", err)
|
linkChat, err := ctx.ResolveUsername(chatUsername)
|
||||||
ctx.Reply(update, ext.ReplyTextString("无法解析 Chat ID"), nil)
|
if err != nil {
|
||||||
return dispatcher.EndGroups
|
common.Log.Errorf("解析用户名失败: %s", err)
|
||||||
}
|
ctx.Reply(update, ext.ReplyTextString("解析用户名失败"), nil)
|
||||||
if linkChat == nil {
|
return dispatcher.EndGroups
|
||||||
logger.L.Errorf("无法找到聊天: %s", chatUsername)
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString("无法找到聊天"), nil)
|
if linkChat == nil {
|
||||||
|
common.Log.Errorf("无法找到聊天: %s", chatUsername)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无法找到聊天"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
linkChatID = linkChat.GetID()
|
||||||
|
} else if len(strSlice) == 4 {
|
||||||
|
chatID, err := strconv.Atoi(strSlice[2])
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("解析 Chat ID 失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("解析 Chat ID 失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
linkChatID = int64(chatID)
|
||||||
|
} else {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无法解析链接"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("获取用户失败: %s", err)
|
common.Log.Errorf("获取用户失败: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -62,32 +77,30 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
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("回复失败: %s", err)
|
common.Log.Errorf("回复失败: %s", err)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := FileFromMessage(ctx, linkChat.GetID(), messageID, "")
|
file, err := FileFromMessage(ctx, linkChatID, messageID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("获取文件失败: %s", err)
|
common.Log.Errorf("获取文件失败: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取文件失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString("获取文件失败: "+err.Error()), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
// TODO: Better file name
|
|
||||||
if file.FileName == "" {
|
if file.FileName == "" {
|
||||||
logger.L.Warnf("文件名为空,使用生成的名称")
|
file.FileName = GenFileNameFromMessage(*update.EffectiveMessage.Message, file)
|
||||||
file.FileName = fmt.Sprintf("%d_%d_%s", linkChat.GetID(), messageID, file.Hash())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
receivedFile := &types.ReceivedFile{
|
receivedFile := &dao.ReceivedFile{
|
||||||
Processing: false,
|
Processing: false,
|
||||||
FileName: file.FileName,
|
FileName: file.FileName,
|
||||||
ChatID: linkChat.GetID(),
|
ChatID: linkChatID,
|
||||||
MessageID: messageID,
|
MessageID: messageID,
|
||||||
ReplyMessageID: replied.ID,
|
ReplyMessageID: replied.ID,
|
||||||
ReplyChatID: update.GetUserChat().GetID(),
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
}
|
}
|
||||||
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
||||||
logger.L.Errorf("保存接收的文件失败: %s", err)
|
common.Log.Errorf("保存接收的文件失败: %s", err)
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: "无法保存文件: " + err.Error(),
|
Message: "无法保存文件: " + err.Error(),
|
||||||
ID: replied.ID,
|
ID: replied.ID,
|
||||||
@@ -95,7 +108,7 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if !user.Silent || user.DefaultStorage == "" {
|
if !user.Silent || user.DefaultStorage == "" {
|
||||||
return ProvideSelectMessage(ctx, update, file, linkChat.GetID(), messageID, replied.ID)
|
return ProvideSelectMessage(ctx, update, file.FileName, linkChatID, messageID, replied.ID)
|
||||||
}
|
}
|
||||||
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
@@ -103,7 +116,7 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
File: file,
|
File: file,
|
||||||
StorageName: user.DefaultStorage,
|
StorageName: user.DefaultStorage,
|
||||||
UserID: user.ChatID,
|
UserID: user.ChatID,
|
||||||
FileChatID: linkChat.GetID(),
|
FileChatID: linkChatID,
|
||||||
FileMessageID: messageID,
|
FileMessageID: messageID,
|
||||||
ReplyMessageID: replied.ID,
|
ReplyMessageID: replied.ID,
|
||||||
ReplyChatID: update.GetUserChat().GetID(),
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
|
|||||||
141
bot/handle_rule.go
Normal file
141
bot/handle_rule.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sendRuleHelp(ctx *ext.Context, update *ext.Update, userChatID int64) error {
|
||||||
|
user, err := dao.GetUserByChatID(userChatID)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取用户规则失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取用户规则失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextStyledTextArray(
|
||||||
|
[]styling.StyledTextOption{
|
||||||
|
styling.Bold("使用方法: /rule <操作> <参数...>"),
|
||||||
|
styling.Bold(fmt.Sprintf("\n当前已%s规则模式", map[bool]string{true: "启用", false: "禁用"}[user.ApplyRule])),
|
||||||
|
styling.Plain("\n\n可用操作:\n"),
|
||||||
|
styling.Code("switch"),
|
||||||
|
styling.Plain(" - 开关规则模式\n"),
|
||||||
|
styling.Code("add"),
|
||||||
|
styling.Plain(" <类型> <数据> <存储名> <路径> - 添加规则\n"),
|
||||||
|
styling.Code("del"),
|
||||||
|
styling.Plain(" <规则ID> - 删除规则\n"),
|
||||||
|
styling.Plain("\n当前已添加的规则:\n"),
|
||||||
|
styling.Blockquote(func() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, rule := range user.Rules {
|
||||||
|
ruleText := fmt.Sprintf("%s %s %s %s", rule.Type, rule.Data, rule.StorageName, rule.DirPath)
|
||||||
|
sb.WriteString(fmt.Sprintf("%d: %s\n", rule.ID, ruleText))
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}(), true),
|
||||||
|
},
|
||||||
|
), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruleCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||||
|
if len(args) < 2 {
|
||||||
|
return sendRuleHelp(ctx, update, update.GetUserChat().GetID())
|
||||||
|
}
|
||||||
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取用户失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
switch args[1] {
|
||||||
|
case "switch":
|
||||||
|
// /rule switch
|
||||||
|
return switchApplyRule(ctx, update, user)
|
||||||
|
case "add":
|
||||||
|
// /rule add <type> <data> <storage> <dirpath>
|
||||||
|
if len(args) < 6 {
|
||||||
|
return sendRuleHelp(ctx, update, user.ChatID)
|
||||||
|
}
|
||||||
|
return addRule(ctx, update, user, args)
|
||||||
|
case "del":
|
||||||
|
// /rule del <id>
|
||||||
|
if len(args) < 3 {
|
||||||
|
return sendRuleHelp(ctx, update, user.ChatID)
|
||||||
|
}
|
||||||
|
ruleID := args[2]
|
||||||
|
id, err := strconv.Atoi(ruleID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无效的规则ID"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if err := dao.DeleteRule(uint(id)); err != nil {
|
||||||
|
common.Log.Errorf("删除规则失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("删除规则失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("删除规则成功"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
default:
|
||||||
|
return sendRuleHelp(ctx, update, user.ChatID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func switchApplyRule(ctx *ext.Context, update *ext.Update, user *dao.User) error {
|
||||||
|
applyRule := !user.ApplyRule
|
||||||
|
if err := dao.UpdateUserApplyRule(user.ChatID, applyRule); err != nil {
|
||||||
|
common.Log.Errorf("更新用户失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("更新用户失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if applyRule {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("已启用规则模式"), nil)
|
||||||
|
} else {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("已禁用规则模式"), nil)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRule(ctx *ext.Context, update *ext.Update, user *dao.User, args []string) error {
|
||||||
|
// /rule add <type> <data> <storage> <dirpath>
|
||||||
|
ruleType := args[2]
|
||||||
|
ruleData := args[3]
|
||||||
|
storageName := args[4]
|
||||||
|
dirPath := args[5]
|
||||||
|
|
||||||
|
if !slice.Contain(types.RuleTypes, types.RuleType(ruleType)) {
|
||||||
|
var ruleTypesStylingArray []styling.StyledTextOption
|
||||||
|
ruleTypesStylingArray = append(ruleTypesStylingArray, styling.Bold("无效的规则类型, 可用类型:\n"))
|
||||||
|
for i, ruleType := range types.RuleTypes {
|
||||||
|
ruleTypesStylingArray = append(ruleTypesStylingArray, styling.Code(string(ruleType)))
|
||||||
|
if i != len(types.RuleTypes)-1 {
|
||||||
|
ruleTypesStylingArray = append(ruleTypesStylingArray, styling.Plain(", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextStyledTextArray(ruleTypesStylingArray), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
rule := &dao.Rule{
|
||||||
|
Type: ruleType,
|
||||||
|
Data: ruleData,
|
||||||
|
StorageName: storageName,
|
||||||
|
DirPath: dirPath,
|
||||||
|
UserID: user.ID,
|
||||||
|
}
|
||||||
|
if err := dao.CreateRule(rule); err != nil {
|
||||||
|
common.Log.Errorf("添加规则失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("添加规则失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("添加规则成功"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
263
bot/handle_save.go
Normal file
263
bot/handle_save.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
|
"github.com/krau/SaveAny-Bot/queue"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sendSaveHelp(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
helpText := `
|
||||||
|
使用方法:
|
||||||
|
|
||||||
|
1. 使用该命令回复要保存的文件, 可选文件名参数.
|
||||||
|
示例:
|
||||||
|
/save custom_file_name.mp4
|
||||||
|
|
||||||
|
2. 设置默认存储后, 发送 /save <频道ID/用户名> <消息ID范围> 来批量保存文件. 遵从存储规则, 若未匹配到任何规则则使用默认存储.
|
||||||
|
示例:
|
||||||
|
/save @moreacg 114-514
|
||||||
|
`
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(helpText), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||||
|
if len(args) >= 3 {
|
||||||
|
return handleBatchSave(ctx, update, args[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
replyToMsgID := func() int {
|
||||||
|
res, ok := update.EffectiveMessage.GetReplyTo()
|
||||||
|
if !ok || res == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
replyHeader, ok := res.(*tg.MessageReplyHeader)
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
replyToMsgID, ok := replyHeader.GetReplyToMsgID()
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return replyToMsgID
|
||||||
|
}()
|
||||||
|
if replyToMsgID == 0 {
|
||||||
|
return sendSaveHelp(ctx, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
common.Log.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 {
|
||||||
|
common.Log.Errorf("获取消息失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无法获取消息"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
supported, _ := supportedMediaFilter(msg)
|
||||||
|
if !supported {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("不支持的消息类型或消息中没有文件"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("回复失败: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdText := update.EffectiveMessage.Text
|
||||||
|
customFileName := strings.TrimSpace(strings.TrimPrefix(cmdText, "/save"))
|
||||||
|
|
||||||
|
file, err := FileFromMessage(ctx, update.EffectiveChat().GetID(), msg.ID, customFileName)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取文件失败: %s", err)
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("获取文件失败: %s", err),
|
||||||
|
ID: replied.ID,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.FileName == "" {
|
||||||
|
file.FileName = GenFileNameFromMessage(*msg, file)
|
||||||
|
}
|
||||||
|
receivedFile := &dao.ReceivedFile{
|
||||||
|
Processing: false,
|
||||||
|
FileName: file.FileName,
|
||||||
|
ChatID: update.EffectiveChat().GetID(),
|
||||||
|
MessageID: replyToMsgID,
|
||||||
|
ReplyMessageID: replied.ID,
|
||||||
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
||||||
|
common.Log.Errorf("保存接收的文件失败: %s", err)
|
||||||
|
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("保存接收的文件失败: %s", err),
|
||||||
|
ID: replied.ID,
|
||||||
|
}); err != nil {
|
||||||
|
common.Log.Errorf("编辑消息失败: %s", err)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if !user.Silent || user.DefaultStorage == "" {
|
||||||
|
return ProvideSelectMessage(ctx, update, file.FileName, update.EffectiveChat().GetID(), msg.ID, replied.ID)
|
||||||
|
}
|
||||||
|
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||||
|
Ctx: ctx,
|
||||||
|
Status: types.Pending,
|
||||||
|
File: file,
|
||||||
|
StorageName: user.DefaultStorage,
|
||||||
|
FileChatID: update.EffectiveChat().GetID(),
|
||||||
|
ReplyMessageID: replied.ID,
|
||||||
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
|
FileMessageID: msg.ID,
|
||||||
|
UserID: user.ChatID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error {
|
||||||
|
// args: [0] = @channel, [1] = 114-514
|
||||||
|
chatArg := args[0]
|
||||||
|
var chatID int64
|
||||||
|
var err error
|
||||||
|
msgIdSlice := strings.Split(args[1], "-")
|
||||||
|
if len(msgIdSlice) != 2 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无效的消息ID范围"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
minMsgID, minerr := strconv.ParseInt(msgIdSlice[0], 10, 64)
|
||||||
|
maxMsgID, maxerr := strconv.ParseInt(msgIdSlice[1], 10, 64)
|
||||||
|
if minerr != nil || maxerr != nil {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无效的消息ID范围"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if minMsgID > maxMsgID || minMsgID <= 0 || maxMsgID <= 0 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无效的消息ID范围"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取用户失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if user.DefaultStorage == "" {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(chatArg, "@") {
|
||||||
|
chatUsername := strings.TrimPrefix(chatArg, "@")
|
||||||
|
chat, err := ctx.ResolveUsername(chatUsername)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("解析频道用户名失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("解析频道用户名失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if chat == nil {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无法找到聊天"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
chatID = chat.GetID()
|
||||||
|
} else {
|
||||||
|
chatID, err = strconv.ParseInt(chatArg, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无效的频道ID或用户名"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if chatID == 0 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无效的频道ID或用户名"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
replied, err := ctx.Reply(update, ext.ReplyTextString("正在批量保存..."), nil)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("回复失败: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
total := maxMsgID - minMsgID + 1
|
||||||
|
successadd := 0
|
||||||
|
failedGetFile := 0
|
||||||
|
failedGetMsg := 0
|
||||||
|
failedSaveDB := 0
|
||||||
|
for i := minMsgID; i <= maxMsgID; i++ {
|
||||||
|
file, err := FileFromMessage(ctx, chatID, int(i), "")
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取文件失败: %s", err)
|
||||||
|
failedGetFile++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if file.FileName == "" {
|
||||||
|
message, err := GetTGMessage(ctx, chatID, int(i))
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取消息失败: %s", err)
|
||||||
|
failedGetMsg++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file.FileName = GenFileNameFromMessage(*message, file)
|
||||||
|
}
|
||||||
|
receivedFile := &dao.ReceivedFile{
|
||||||
|
Processing: false,
|
||||||
|
FileName: file.FileName,
|
||||||
|
ChatID: chatID,
|
||||||
|
MessageID: int(i),
|
||||||
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
|
ReplyMessageID: 0,
|
||||||
|
}
|
||||||
|
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
||||||
|
common.Log.Errorf("保存接收的文件失败: %s", err)
|
||||||
|
failedSaveDB++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
task := &types.Task{
|
||||||
|
Ctx: ctx,
|
||||||
|
Status: types.Pending,
|
||||||
|
File: file,
|
||||||
|
StorageName: user.DefaultStorage,
|
||||||
|
FileChatID: chatID,
|
||||||
|
FileMessageID: int(i),
|
||||||
|
UserID: user.ChatID,
|
||||||
|
ReplyMessageID: 0,
|
||||||
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
|
}
|
||||||
|
queue.AddTask(task)
|
||||||
|
successadd++
|
||||||
|
}
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("批量保存完成\n成功添加: %d/%d\n获取文件失败: %d\n获取消息失败: %d\n保存数据库失败: %d", successadd, total, failedGetFile, failedGetMsg, failedSaveDB),
|
||||||
|
ID: replied.ID,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
30
bot/handle_silent.go
Normal file
30
bot/handle_silent.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
func silent(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取用户失败: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if !user.Silent && user.DefaultStorage == "" {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("请先使用 /storage 设置默认存储位置"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
user.Silent = !user.Silent
|
||||||
|
if err := dao.UpdateUser(user); err != nil {
|
||||||
|
common.Log.Errorf("更新用户失败: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("更新用户失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已%s静默模式", map[bool]string{true: "开启", false: "关闭"}[user.Silent])), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
40
bot/handle_start.go
Normal file
40
bot/handle_start.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
func start(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
if err := dao.CreateUser(update.GetUserChat().GetID()); err != nil {
|
||||||
|
common.Log.Errorf("创建用户失败: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
return help(ctx, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
const helpText string = `
|
||||||
|
Save Any Bot - 转存你的 Telegram 文件
|
||||||
|
版本: %s , 提交: %s
|
||||||
|
命令:
|
||||||
|
/start - 开始使用
|
||||||
|
/help - 显示帮助
|
||||||
|
/silent - 开关静默模式
|
||||||
|
/storage - 设置默认存储位置
|
||||||
|
/save [自定义文件名] - 保存文件
|
||||||
|
|
||||||
|
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
|
||||||
|
|
||||||
|
默认存储位置: 在静默模式下保存到的位置
|
||||||
|
|
||||||
|
向 Bot 发送(转发)文件, 或发送一个公开频道的消息链接以保存文件
|
||||||
|
`
|
||||||
|
|
||||||
|
func help(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf(helpText, common.Version, common.GitCommit[:7])), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
99
bot/handle_storage.go
Normal file
99
bot/handle_storage.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func storageCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
userChatID := update.GetUserChat().GetID()
|
||||||
|
storages := storage.GetUserStorages(userChatID)
|
||||||
|
if len(storages) == 0 {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
markup, err := getSetDefaultStorageMarkup(userChatID, storages)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("Failed to get markup: %s", err)
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("获取存储位置失败"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("请选择要设为默认的存储位置"), &ext.ReplyOpts{
|
||||||
|
Markup: markup,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDefaultStorage(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
args := strings.Split(string(update.CallbackQuery.Data), " ")
|
||||||
|
userID, _ := strconv.Atoi(args[1])
|
||||||
|
if userID != int(update.CallbackQuery.GetUserID()) {
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "你没有权限",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
cbDataId, _ := strconv.Atoi(args[2])
|
||||||
|
storageName, err := dao.GetCallbackData(uint(cbDataId))
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("获取回调数据失败: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "获取回调数据失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedStorage, err := storage.GetStorageByName(storageName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
common.Log.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 {
|
||||||
|
common.Log.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 {
|
||||||
|
common.Log.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
|
||||||
|
}
|
||||||
114
bot/handle_telegraph.go
Normal file
114
bot/handle_telegraph.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/celestix/telegraph-go/v2"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TelegraphClient *telegraph.TelegraphClient
|
||||||
|
TelegraphUrlRegexString = `https://telegra.ph/.*`
|
||||||
|
TelegraphUrlRegex = regexp.MustCompile(TelegraphUrlRegexString)
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitTelegraphClient() {
|
||||||
|
var httpClient *http.Client
|
||||||
|
if config.Cfg.Telegram.Proxy.Enable {
|
||||||
|
proxyUrl, err := url.Parse(config.Cfg.Telegram.Proxy.URL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error parsing proxy URL:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proxy := http.ProxyURL(proxyUrl)
|
||||||
|
httpClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: proxy,
|
||||||
|
},
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
httpClient = &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TelegraphClient = telegraph.GetTelegraphClient(&telegraph.ClientOpt{HttpClient: httpClient})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTelegraph(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
common.Log.Trace("Got telegraph link")
|
||||||
|
tgphUrl := TelegraphUrlRegex.FindString(update.EffectiveMessage.Text)
|
||||||
|
if tgphUrl == "" {
|
||||||
|
return dispatcher.ContinueGroups
|
||||||
|
}
|
||||||
|
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件..."), nil)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("回复失败: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
common.Log.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
|
||||||
|
}
|
||||||
|
|
||||||
|
tgphPath := strings.Split(tgphUrl, "/")[len(strings.Split(tgphUrl, "/"))-1]
|
||||||
|
fileName, err := url.PathUnescape(tgphPath)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("解析 Telegraph 路径失败: %s", err)
|
||||||
|
fileName = tgphPath
|
||||||
|
}
|
||||||
|
|
||||||
|
record := &dao.ReceivedFile{
|
||||||
|
Processing: false,
|
||||||
|
FileName: fileName,
|
||||||
|
ChatID: update.EffectiveChat().GetID(),
|
||||||
|
MessageID: update.EffectiveMessage.GetID(),
|
||||||
|
ReplyMessageID: replied.ID,
|
||||||
|
ReplyChatID: update.EffectiveChat().GetID(),
|
||||||
|
IsTelegraph: true,
|
||||||
|
TelegraphURL: tgphUrl,
|
||||||
|
}
|
||||||
|
if err := dao.SaveReceivedFile(record); err != nil {
|
||||||
|
common.Log.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, fileName, update.EffectiveChat().GetID(), update.EffectiveMessage.GetID(), replied.ID)
|
||||||
|
}
|
||||||
|
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||||
|
Ctx: ctx,
|
||||||
|
Status: types.Pending,
|
||||||
|
StorageName: user.DefaultStorage,
|
||||||
|
UserID: user.ChatID,
|
||||||
|
ReplyMessageID: replied.ID,
|
||||||
|
ReplyChatID: update.GetUserChat().GetID(),
|
||||||
|
IsTelegraph: true,
|
||||||
|
TelegraphURL: tgphUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
428
bot/handlers.go
428
bot/handlers.go
@@ -1,25 +1,10 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
|
||||||
"github.com/gotd/td/telegram/message/entity"
|
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
|
||||||
"github.com/gotd/td/tg"
|
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/dispatcher/handlers"
|
"github.com/celestix/gotgproto/dispatcher/handlers"
|
||||||
"github.com/celestix/gotgproto/dispatcher/handlers/filters"
|
"github.com/celestix/gotgproto/dispatcher/handlers/filters"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
|
||||||
"github.com/krau/SaveAny-Bot/dao"
|
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
|
||||||
"github.com/krau/SaveAny-Bot/queue"
|
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
|
func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
|
||||||
@@ -29,413 +14,20 @@ func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
|
|||||||
dispatcher.AddHandler(handlers.NewCommand("silent", silent))
|
dispatcher.AddHandler(handlers.NewCommand("silent", silent))
|
||||||
dispatcher.AddHandler(handlers.NewCommand("storage", storageCmd))
|
dispatcher.AddHandler(handlers.NewCommand("storage", storageCmd))
|
||||||
dispatcher.AddHandler(handlers.NewCommand("save", saveCmd))
|
dispatcher.AddHandler(handlers.NewCommand("save", saveCmd))
|
||||||
|
dispatcher.AddHandler(handlers.NewCommand("dir", dirCmd))
|
||||||
|
dispatcher.AddHandler(handlers.NewCommand("rule", ruleCmd))
|
||||||
linkRegexFilter, err := filters.Message.Regex(linkRegexString)
|
linkRegexFilter, err := filters.Message.Regex(linkRegexString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Panicf("创建正则表达式过滤器失败: %s", err)
|
common.Log.Panicf("创建正则表达式过滤器失败: %s", err)
|
||||||
}
|
}
|
||||||
dispatcher.AddHandler(handlers.NewMessage(linkRegexFilter, handleLinkMessage))
|
dispatcher.AddHandler(handlers.NewMessage(linkRegexFilter, handleLinkMessage))
|
||||||
|
telegraphUrlRegexFilter, err := filters.Message.Regex(TelegraphUrlRegexString)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Panicf("创建 Telegraph URL 正则表达式过滤器失败: %s", err)
|
||||||
|
}
|
||||||
|
dispatcher.AddHandler(handlers.NewMessage(telegraphUrlRegexFilter, handleTelegraph))
|
||||||
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.NewCallbackQuery(filters.CallbackQuery.Prefix("set_default"), setDefaultStorage))
|
||||||
|
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("cancel"), cancelTask))
|
||||||
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
|
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
|
||||||
}
|
}
|
||||||
|
|
||||||
const noPermissionText string = `
|
|
||||||
您不在白名单中, 无法使用此 Bot.
|
|
||||||
您可以部署自己的实例: https://github.com/krau/SaveAny-Bot
|
|
||||||
`
|
|
||||||
|
|
||||||
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
|
||||||
userID := update.GetUserChat().GetID()
|
|
||||||
if !slice.Contain(config.Cfg.GetUsersID(), userID) {
|
|
||||||
ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
return dispatcher.ContinueGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
func start(ctx *ext.Context, update *ext.Update) error {
|
|
||||||
if err := dao.CreateUser(update.GetUserChat().GetID()); err != nil {
|
|
||||||
logger.L.Errorf("创建用户失败: %s", err)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
return help(ctx, update)
|
|
||||||
}
|
|
||||||
|
|
||||||
const helpText string = `
|
|
||||||
Save Any Bot - 转存你的 Telegram 文件
|
|
||||||
命令:
|
|
||||||
/start - 开始使用
|
|
||||||
/help - 显示帮助
|
|
||||||
/silent - 开关静默模式
|
|
||||||
/storage - 设置默认存储位置
|
|
||||||
/save [自定义文件名] - 保存文件
|
|
||||||
|
|
||||||
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
|
|
||||||
|
|
||||||
默认存储位置: 在静默模式下保存到的位置
|
|
||||||
|
|
||||||
向 Bot 发送(转发)文件, 或发送一个公开频道的消息链接以保存文件
|
|
||||||
`
|
|
||||||
|
|
||||||
func help(ctx *ext.Context, update *ext.Update) error {
|
|
||||||
ctx.Reply(update, ext.ReplyTextString(helpText), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
func silent(ctx *ext.Context, update *ext.Update) error {
|
|
||||||
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Errorf("获取用户失败: %s", err)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
if !user.Silent && user.DefaultStorage == "" {
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("请先使用 /storage 设置默认存储位置"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
user.Silent = !user.Silent
|
|
||||||
if err := dao.UpdateUser(user); err != nil {
|
|
||||||
logger.L.Errorf("更新用户失败: %s", err)
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("更新用户失败"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已%s静默模式", map[bool]string{true: "开启", false: "关闭"}[user.Silent])), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
|
||||||
res, ok := update.EffectiveMessage.GetReplyTo()
|
|
||||||
if !ok || res == nil {
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
replyHeader, ok := res.(*tg.MessageReplyHeader)
|
|
||||||
if !ok {
|
|
||||||
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
replyToMsgID, ok := replyHeader.GetReplyToMsgID()
|
|
||||||
if !ok {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
if !supported {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdText := update.EffectiveMessage.Text
|
|
||||||
customFileName := strings.TrimSpace(strings.TrimPrefix(cmdText, "/save"))
|
|
||||||
|
|
||||||
file, err := FileFromMessage(ctx, update.EffectiveChat().GetID(), msg.ID, customFileName)
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Errorf("获取文件失败: %s", err)
|
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
|
||||||
Message: fmt.Sprintf("获取文件失败: %s", err),
|
|
||||||
ID: replied.ID,
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: better file name
|
|
||||||
if file.FileName == "" {
|
|
||||||
file.FileName = fmt.Sprintf("%d_%d_%s", update.EffectiveChat().GetID(), replyToMsgID, file.Hash())
|
|
||||||
}
|
|
||||||
receivedFile := &types.ReceivedFile{
|
|
||||||
Processing: false,
|
|
||||||
FileName: file.FileName,
|
|
||||||
ChatID: update.EffectiveChat().GetID(),
|
|
||||||
MessageID: replyToMsgID,
|
|
||||||
ReplyMessageID: replied.ID,
|
|
||||||
ReplyChatID: update.GetUserChat().GetID(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
|
||||||
logger.L.Errorf("保存接收的文件失败: %s", err)
|
|
||||||
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
|
||||||
Message: fmt.Sprintf("保存接收的文件失败: %s", err),
|
|
||||||
ID: replied.ID,
|
|
||||||
}); err != nil {
|
|
||||||
logger.L.Errorf("编辑消息失败: %s", err)
|
|
||||||
}
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
if !user.Silent || user.DefaultStorage == "" {
|
|
||||||
return ProvideSelectMessage(ctx, update, file, update.EffectiveChat().GetID(), msg.ID, replied.ID)
|
|
||||||
}
|
|
||||||
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
|
||||||
Ctx: ctx,
|
|
||||||
Status: types.Pending,
|
|
||||||
File: file,
|
|
||||||
StorageName: user.DefaultStorage,
|
|
||||||
FileChatID: update.EffectiveChat().GetID(),
|
|
||||||
ReplyMessageID: replied.ID,
|
|
||||||
ReplyChatID: update.GetUserChat().GetID(),
|
|
||||||
FileMessageID: msg.ID,
|
|
||||||
UserID: user.ChatID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func storageCmd(ctx *ext.Context, update *ext.Update) error {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
|
|
||||||
logger.L.Trace("Got media: ", update.EffectiveMessage.Media.TypeName())
|
|
||||||
supported, err := supportedMediaFilter(update.EffectiveMessage.Message)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !supported {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Errorf("回复失败: %s", err)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
media := update.EffectiveMessage.Media
|
|
||||||
file, err := FileFromMedia(media, "")
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Errorf("获取文件失败: %s", err)
|
|
||||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil)
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
if file.FileName == "" {
|
|
||||||
file.FileName = fmt.Sprintf("%d_%d_%s", update.EffectiveChat().GetID(), update.EffectiveMessage.ID, file.Hash())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dao.SaveReceivedFile(&types.ReceivedFile{
|
|
||||||
Processing: false,
|
|
||||||
FileName: file.FileName,
|
|
||||||
ChatID: update.EffectiveChat().GetID(),
|
|
||||||
MessageID: update.EffectiveMessage.ID,
|
|
||||||
ReplyMessageID: msg.ID,
|
|
||||||
ReplyChatID: update.GetUserChat().GetID(),
|
|
||||||
}); err != nil {
|
|
||||||
logger.L.Errorf("添加接收的文件失败: %s", err)
|
|
||||||
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
|
||||||
Message: fmt.Sprintf("添加接收的文件失败: %s", err),
|
|
||||||
ID: msg.ID,
|
|
||||||
}); err != nil {
|
|
||||||
logger.L.Errorf("编辑消息失败: %s", err)
|
|
||||||
}
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
if !user.Silent || user.DefaultStorage == "" {
|
|
||||||
return ProvideSelectMessage(ctx, update, file, update.EffectiveChat().GetID(), update.EffectiveMessage.ID, msg.ID)
|
|
||||||
}
|
|
||||||
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
|
||||||
Ctx: ctx,
|
|
||||||
Status: types.Pending,
|
|
||||||
File: file,
|
|
||||||
StorageName: user.DefaultStorage,
|
|
||||||
FileChatID: update.EffectiveChat().GetID(),
|
|
||||||
ReplyMessageID: msg.ID,
|
|
||||||
ReplyChatID: update.GetUserChat().GetID(),
|
|
||||||
FileMessageID: update.EffectiveMessage.ID,
|
|
||||||
UserID: user.ChatID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
|
||||||
if !slice.Contain(config.Cfg.GetUsersID(), update.CallbackQuery.UserID) {
|
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
|
||||||
QueryID: update.CallbackQuery.QueryID,
|
|
||||||
Alert: true,
|
|
||||||
Message: "你没有权限",
|
|
||||||
CacheTime: 5,
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
args := strings.Split(string(update.CallbackQuery.Data), " ")
|
|
||||||
fileChatID, _ := strconv.Atoi(args[1])
|
|
||||||
fileMessageID, _ := strconv.Atoi(args[2])
|
|
||||||
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 {
|
|
||||||
logger.L.Errorf("获取记录失败: %s", err)
|
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
|
||||||
QueryID: update.CallbackQuery.QueryID,
|
|
||||||
Alert: true,
|
|
||||||
Message: "查询记录失败",
|
|
||||||
CacheTime: 5,
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
if update.CallbackQuery.MsgID != record.ReplyMessageID {
|
|
||||||
record.ReplyMessageID = update.CallbackQuery.MsgID
|
|
||||||
if err := dao.SaveReceivedFile(record); err != nil {
|
|
||||||
logger.L.Errorf("更新接收的文件失败: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
file, err := FileFromMessage(ctx, record.ChatID, record.MessageID, record.FileName)
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Errorf("获取消息中的文件失败: %s", err)
|
|
||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
|
||||||
QueryID: update.CallbackQuery.QueryID,
|
|
||||||
Alert: true,
|
|
||||||
Message: fmt.Sprintf("获取消息中的文件失败: %s", err),
|
|
||||||
CacheTime: 5,
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
queue.AddTask(types.Task{
|
|
||||||
Ctx: ctx,
|
|
||||||
Status: types.Pending,
|
|
||||||
File: file,
|
|
||||||
StorageName: storageName,
|
|
||||||
FileChatID: record.ChatID,
|
|
||||||
ReplyMessageID: record.ReplyMessageID,
|
|
||||||
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{
|
|
||||||
Message: text,
|
|
||||||
Entities: entities,
|
|
||||||
ID: record.ReplyMessageID,
|
|
||||||
})
|
|
||||||
return dispatcher.EndGroups
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,17 +3,35 @@ package bot
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/gotd/contrib/middleware/floodwait"
|
"github.com/gotd/contrib/middleware/floodwait"
|
||||||
"github.com/gotd/contrib/middleware/ratelimit"
|
"github.com/gotd/contrib/middleware/ratelimit"
|
||||||
"github.com/gotd/td/telegram"
|
"github.com/gotd/td/telegram"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FloodWaitMiddleware() []telegram.Middleware {
|
func FloodWaitMiddleware() []telegram.Middleware {
|
||||||
waiter := floodwait.NewSimpleWaiter().WithMaxRetries(5)
|
waiter := floodwait.NewSimpleWaiter().WithMaxRetries(uint(config.Cfg.Telegram.FloodRetry))
|
||||||
ratelimiter := ratelimit.New(rate.Every(time.Millisecond*100), 5)
|
ratelimiter := ratelimit.New(rate.Every(time.Millisecond*100), 5)
|
||||||
return []telegram.Middleware{
|
return []telegram.Middleware{
|
||||||
waiter,
|
waiter,
|
||||||
ratelimiter,
|
ratelimiter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const noPermissionText string = `
|
||||||
|
您不在白名单中, 无法使用此 Bot.
|
||||||
|
您可以部署自己的实例: https://github.com/krau/SaveAny-Bot
|
||||||
|
`
|
||||||
|
|
||||||
|
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
userID := update.GetUserChat().GetID()
|
||||||
|
if !slice.Contain(config.Cfg.GetUsersID(), userID) {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
return dispatcher.ContinueGroups
|
||||||
|
}
|
||||||
|
|||||||
154
bot/utils.go
154
bot/utils.go
@@ -3,16 +3,18 @@ package bot
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/common"
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/dao"
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
"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/storage"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
@@ -24,6 +26,7 @@ var (
|
|||||||
ErrEmptyPhotoSize = errors.New("photo size is empty")
|
ErrEmptyPhotoSize = errors.New("photo size is empty")
|
||||||
ErrEmptyPhotoSizes = errors.New("photo size slice is empty")
|
ErrEmptyPhotoSizes = errors.New("photo size slice is empty")
|
||||||
ErrNoStorages = errors.New("no available storage")
|
ErrNoStorages = errors.New("no available storage")
|
||||||
|
ErrEmptyMessage = errors.New("message is empty")
|
||||||
)
|
)
|
||||||
|
|
||||||
func supportedMediaFilter(m *tg.Message) (bool, error) {
|
func supportedMediaFilter(m *tg.Message) (bool, error) {
|
||||||
@@ -40,23 +43,26 @@ func supportedMediaFilter(m *tg.Message) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for callback data
|
|
||||||
var storageHashName = map[string]string{}
|
|
||||||
|
|
||||||
func getSelectStorageMarkup(userChatID int64, fileChatID, fileMessageID int) (*tg.ReplyInlineMarkup, error) {
|
func getSelectStorageMarkup(userChatID int64, fileChatID, fileMessageID int) (*tg.ReplyInlineMarkup, error) {
|
||||||
user, err := dao.GetUserByChatID(userChatID)
|
user, err := dao.GetUserByChatID(userChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to get user by chat ID: %d, error: %w", userChatID, err)
|
||||||
}
|
}
|
||||||
storages := storage.GetUserStorages(user.ChatID)
|
storages := storage.GetUserStorages(user.ChatID)
|
||||||
|
if len(storages) == 0 {
|
||||||
|
return nil, ErrNoStorages
|
||||||
|
}
|
||||||
|
|
||||||
buttons := make([]tg.KeyboardButtonClass, 0)
|
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||||
for _, storage := range storages {
|
for _, storage := range storages {
|
||||||
nameHash := common.HashString(storage.Name())
|
cbData := fmt.Sprintf("%d %d %s 0", fileChatID, fileMessageID, storage.Name()) // 0 for empty dir id
|
||||||
storageHashName[nameHash] = storage.Name()
|
cbDataId, err := dao.CreateCallbackData(cbData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create callback data: %w", err)
|
||||||
|
}
|
||||||
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
||||||
Text: storage.Name(),
|
Text: storage.Name(),
|
||||||
Data: []byte(fmt.Sprintf("add %d %d %s", fileChatID, fileMessageID, nameHash)),
|
Data: fmt.Appendf(nil, "add %d", cbDataId),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
markup := &tg.ReplyInlineMarkup{}
|
markup := &tg.ReplyInlineMarkup{}
|
||||||
@@ -68,14 +74,19 @@ func getSelectStorageMarkup(userChatID int64, fileChatID, fileMessageID int) (*t
|
|||||||
return markup, nil
|
return markup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSetDefaultStorageMarkup(userChatID int64, storages []storage.Storage) *tg.ReplyInlineMarkup {
|
func getSelectDirMarkup(fileChatID, fileMessageID int, storageName string, dirs []dao.Dir) (*tg.ReplyInlineMarkup, error) {
|
||||||
buttons := make([]tg.KeyboardButtonClass, 0)
|
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||||
for _, storage := range storages {
|
for _, dir := range dirs {
|
||||||
nameHash := common.HashString(storage.Name())
|
if dir.ID == 0 || dir.StorageName != storageName {
|
||||||
storageHashName[nameHash] = storage.Name()
|
return nil, fmt.Errorf("unexpected dir: %v", dir)
|
||||||
|
}
|
||||||
|
cbDataId, err := dao.CreateCallbackData(fmt.Sprintf("%d %d %s %d", fileChatID, fileMessageID, storageName, dir.ID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create callback data: %w", err)
|
||||||
|
}
|
||||||
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
||||||
Text: storage.Name(),
|
Text: dir.Path,
|
||||||
Data: []byte(fmt.Sprintf("set_default %d %s", userChatID, nameHash)),
|
Data: []byte(fmt.Sprintf("add_to_dir %d", cbDataId)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
markup := &tg.ReplyInlineMarkup{}
|
markup := &tg.ReplyInlineMarkup{}
|
||||||
@@ -84,8 +95,28 @@ func getSetDefaultStorageMarkup(userChatID int64, storages []storage.Storage) *t
|
|||||||
row.Buttons = buttons[i:min(i+3, len(buttons))]
|
row.Buttons = buttons[i:min(i+3, len(buttons))]
|
||||||
markup.Rows = append(markup.Rows, row)
|
markup.Rows = append(markup.Rows, row)
|
||||||
}
|
}
|
||||||
return markup
|
return markup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSetDefaultStorageMarkup(userChatID int64, storages []storage.Storage) (*tg.ReplyInlineMarkup, error) {
|
||||||
|
buttons := make([]tg.KeyboardButtonClass, 0)
|
||||||
|
for _, storage := range storages {
|
||||||
|
cbDataId, err := dao.CreateCallbackData(storage.Name())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create callback data: %w", err)
|
||||||
|
}
|
||||||
|
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
||||||
|
Text: storage.Name(),
|
||||||
|
Data: []byte(fmt.Sprintf("set_default %d %d", userChatID, cbDataId)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
markup := &tg.ReplyInlineMarkup{}
|
||||||
|
for i := 0; i < len(buttons); i += 3 {
|
||||||
|
row := tg.KeyboardButtonRow{}
|
||||||
|
row.Buttons = buttons[i:min(i+3, len(buttons))]
|
||||||
|
markup.Rows = append(markup.Rows, row)
|
||||||
|
}
|
||||||
|
return markup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FileFromMedia(media tg.MessageMediaClass, customFileName string) (*types.File, error) {
|
func FileFromMedia(media tg.MessageMediaClass, customFileName string) (*types.File, error) {
|
||||||
@@ -149,12 +180,14 @@ func FileFromMedia(media tg.MessageMediaClass, customFileName string) (*types.Fi
|
|||||||
|
|
||||||
func FileFromMessage(ctx *ext.Context, chatID int64, messageID int, customFileName string) (*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)
|
cachedFile, err := common.CacheGet[*types.File](ctx, key)
|
||||||
var cachedFile types.File
|
|
||||||
err := common.Cache.Get(key, &cachedFile)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return &cachedFile, nil
|
if customFileName != "" {
|
||||||
|
cachedFile.FileName = customFileName
|
||||||
|
}
|
||||||
|
return cachedFile, nil
|
||||||
}
|
}
|
||||||
|
common.Log.Debugf("Getting file: %s", key)
|
||||||
message, err := GetTGMessage(ctx, chatID, messageID)
|
message, err := GetTGMessage(ctx, chatID, messageID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -163,52 +196,60 @@ func FileFromMessage(ctx *ext.Context, chatID int64, messageID int, customFileNa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := common.Cache.Set(key, file, 3600); err != nil {
|
if err := common.CacheSet(ctx, key, file); err != nil {
|
||||||
logger.L.Errorf("Failed to cache file: %s", err)
|
common.Log.Errorf("Failed to cache file: %s", err)
|
||||||
}
|
}
|
||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTGMessage(ctx *ext.Context, chatId int64, messageID int) (*tg.Message, error) {
|
func GetTGMessage(ctx *ext.Context, chatId int64, messageID int) (*tg.Message, error) {
|
||||||
logger.L.Debugf("Fetching message: %d", messageID)
|
key := fmt.Sprintf("message:%d:%d", chatId, messageID)
|
||||||
|
cacheMessage, err := common.CacheGet[*tg.Message](ctx, key)
|
||||||
|
if err == nil {
|
||||||
|
return cacheMessage, nil
|
||||||
|
}
|
||||||
|
common.Log.Debugf("Fetching message: %d", messageID)
|
||||||
messages, err := ctx.GetMessages(chatId, []tg.InputMessageClass{&tg.InputMessageID{ID: messageID}})
|
messages, err := ctx.GetMessages(chatId, []tg.InputMessageClass{&tg.InputMessageID{ID: messageID}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(messages) == 0 {
|
if len(messages) == 0 {
|
||||||
return nil, errors.New("no messages found")
|
return nil, ErrEmptyMessage
|
||||||
}
|
}
|
||||||
msg := messages[0]
|
msg := messages[0]
|
||||||
tgMessage, ok := msg.(*tg.Message)
|
tgMessage, ok := msg.(*tg.Message)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unexpected message type: %T", msg)
|
return nil, fmt.Errorf("unexpected message type: %T", msg)
|
||||||
}
|
}
|
||||||
|
if err := common.CacheSet(ctx, key, tgMessage); err != nil {
|
||||||
|
common.Log.Errorf("Failed to cache message: %s", err)
|
||||||
|
}
|
||||||
return tgMessage, nil
|
return tgMessage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideSelectMessage(ctx *ext.Context, update *ext.Update, file *types.File, chatID int64, fileMsgID, toEditMsgID int) error {
|
func ProvideSelectMessage(ctx *ext.Context, update *ext.Update, fileName string, chatID int64, fileMsgID, toEditMsgID int) error {
|
||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
text := fmt.Sprintf("文件名: %s\n请选择存储位置", file.FileName)
|
text := fmt.Sprintf("文件名: %s\n请选择存储位置", fileName)
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("文件名: "),
|
styling.Plain("文件名: "),
|
||||||
styling.Code(file.FileName),
|
styling.Code(fileName),
|
||||||
styling.Plain("\n请选择存储位置"),
|
styling.Plain("\n请选择存储位置"),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.L.Errorf("Failed to build entity: %s", err)
|
common.Log.Errorf("Failed to build entity: %s", err)
|
||||||
} else {
|
} else {
|
||||||
text, entities = entityBuilder.Complete()
|
text, entities = entityBuilder.Complete()
|
||||||
}
|
}
|
||||||
markup, err := getSelectStorageMarkup(update.EffectiveUser().GetID(), int(chatID), fileMsgID)
|
markup, err := getSelectStorageMarkup(update.GetUserChat().GetID(), int(chatID), fileMsgID)
|
||||||
if errors.Is(err, ErrNoStorages) {
|
if errors.Is(err, ErrNoStorages) {
|
||||||
logger.L.Errorf("Failed to get select storage markup: %s", err)
|
common.Log.Errorf("Failed to get select storage markup: %s", err)
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: "无可用存储",
|
Message: "无可用存储",
|
||||||
ID: toEditMsgID,
|
ID: toEditMsgID,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
logger.L.Errorf("Failed to get select storage markup: %s", err)
|
common.Log.Errorf("Failed to get select storage markup: %s", err)
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: "无法获取存储",
|
Message: "无法获取存储",
|
||||||
ID: toEditMsgID,
|
ID: toEditMsgID,
|
||||||
@@ -222,12 +263,12 @@ func ProvideSelectMessage(ctx *ext.Context, update *ext.Update, file *types.File
|
|||||||
ID: toEditMsgID,
|
ID: toEditMsgID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to reply: %s", err)
|
common.Log.Errorf("Failed to reply: %s", err)
|
||||||
}
|
}
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleSilentAddTask(ctx *ext.Context, update *ext.Update, user *types.User, task *types.Task) error {
|
func HandleSilentAddTask(ctx *ext.Context, update *ext.Update, user *dao.User, task *types.Task) error {
|
||||||
if user.DefaultStorage == "" {
|
if user.DefaultStorage == "" {
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: "请先使用 /storage 设置默认存储位置",
|
Message: "请先使用 /storage 设置默认存储位置",
|
||||||
@@ -235,10 +276,57 @@ func HandleSilentAddTask(ctx *ext.Context, update *ext.Update, user *types.User,
|
|||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
queue.AddTask(*task)
|
queue.AddTask(task)
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", task.FileName(), queue.Len()),
|
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", task.FileName(), queue.Len()),
|
||||||
ID: task.ReplyMessageID,
|
ID: task.ReplyMessageID,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenFileNameFromMessage(message tg.Message, file *types.File) string {
|
||||||
|
if file.FileName != "" {
|
||||||
|
return file.FileName
|
||||||
|
}
|
||||||
|
fileName := genFileNameFromMessageText(message, file)
|
||||||
|
media, ok := message.GetMedia()
|
||||||
|
if !ok {
|
||||||
|
return fileName
|
||||||
|
}
|
||||||
|
ext, ok := extraMediaExt(media)
|
||||||
|
if ok {
|
||||||
|
return fileName + ext
|
||||||
|
}
|
||||||
|
return fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
func genFileNameFromMessageText(message tg.Message, file *types.File) string {
|
||||||
|
text := strings.TrimSpace(message.GetMessage())
|
||||||
|
if text == "" {
|
||||||
|
return file.Hash()
|
||||||
|
}
|
||||||
|
tags := common.ExtractTagsFromText(text)
|
||||||
|
if len(tags) > 0 {
|
||||||
|
return fmt.Sprintf("%s_%s", strings.Join(tags, "_"), strconv.Itoa(message.GetID()))
|
||||||
|
}
|
||||||
|
runes := []rune(text)
|
||||||
|
return string(runes[:min(128, len(runes))])
|
||||||
|
}
|
||||||
|
|
||||||
|
func extraMediaExt(media tg.MessageMediaClass) (string, bool) {
|
||||||
|
switch media := media.(type) {
|
||||||
|
case *tg.MessageMediaDocument:
|
||||||
|
doc, ok := media.Document.AsNotEmpty()
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
ext := mimetype.Lookup(doc.MimeType).Extension()
|
||||||
|
if ext == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return ext, true
|
||||||
|
case *tg.MessageMediaPhoto:
|
||||||
|
return ".jpg", true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|||||||
46
cmd/run.go
46
cmd/run.go
@@ -1,51 +1,67 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/bootstrap"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/bot"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"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/dao"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run(_ *cobra.Command, _ []string) {
|
func Run(_ *cobra.Command, _ []string) {
|
||||||
bootstrap.InitAll()
|
InitAll()
|
||||||
core.Run()
|
core.Run()
|
||||||
|
|
||||||
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, ", exitting...")
|
common.Log.Info(sig, ", exitting...")
|
||||||
defer logger.L.Info("Bye!")
|
defer common.Log.Info("Bye!")
|
||||||
if config.Cfg.NoCleanCache {
|
if config.Cfg.NoCleanCache {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if config.Cfg.Temp.BasePath != "" {
|
if config.Cfg.Temp.BasePath != "" && !config.Cfg.Stream {
|
||||||
for _, path := range []string{"/", ".", "\\", ".."} {
|
if slices.Contains([]string{"/", ".", "\\", ".."}, filepath.Clean(config.Cfg.Temp.BasePath)) {
|
||||||
if filepath.Clean(config.Cfg.Temp.BasePath) == path {
|
common.Log.Error("无效的缓存文件夹: ", config.Cfg.Temp.BasePath)
|
||||||
logger.L.Error("Invalid cache dir: ", config.Cfg.Temp.BasePath)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
currentDir, err := os.Getwd()
|
currentDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Error("Failed to get current dir: ", err)
|
common.Log.Error("获取工作目录失败: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cachePath := filepath.Join(currentDir, config.Cfg.Temp.BasePath)
|
cachePath := filepath.Join(currentDir, config.Cfg.Temp.BasePath)
|
||||||
cachePath, err = filepath.Abs(cachePath)
|
cachePath, err = filepath.Abs(cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Error("Failed to get absolute path: ", err)
|
common.Log.Error("获取缓存绝对路径失败: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.L.Info("Cleaning cache dir: ", cachePath)
|
common.Log.Info("正在清理缓存文件夹: ", cachePath)
|
||||||
if err := os.RemoveAll(cachePath); err != nil {
|
if err := os.RemoveAll(cachePath); err != nil {
|
||||||
logger.L.Error("Failed to clean cache dir: ", err)
|
common.Log.Error("清理缓存失败: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitAll() {
|
||||||
|
if err := config.Init(); err != nil {
|
||||||
|
fmt.Println("加载配置文件失败: ", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
common.InitLogger()
|
||||||
|
common.Log.Info("正在启动 SaveAny-Bot...")
|
||||||
|
dao.Init()
|
||||||
|
storage.LoadStorages()
|
||||||
|
common.Init()
|
||||||
|
bot.Init()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,60 +1,38 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"encoding/gob"
|
"time"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/coocood/freecache"
|
"github.com/eko/gocache/lib/v4/cache"
|
||||||
"github.com/gotd/td/tg"
|
gocachestore "github.com/eko/gocache/store/go_cache/v4"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
gocache "github.com/patrickmn/go-cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommonCache struct {
|
var Cache *cache.Cache[any]
|
||||||
cache *freecache.Cache
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
var Cache *CommonCache
|
|
||||||
|
|
||||||
func initCache() {
|
func initCache() {
|
||||||
gob.Register(types.File{})
|
gocacheClient := gocache.New(time.Hour*1, time.Minute*10)
|
||||||
gob.Register(tg.InputDocumentFileLocation{})
|
gocacheStore := gocachestore.NewGoCache(gocacheClient)
|
||||||
gob.Register(tg.InputPhotoFileLocation{})
|
cacheManager := cache.New[any](gocacheStore)
|
||||||
Cache = &CommonCache{cache: freecache.NewCache(10 * 1024 * 1024)}
|
Cache = cacheManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommonCache) Get(key string, value *types.File) error {
|
func CacheGet[T any](ctx context.Context, key string) (T, error) {
|
||||||
c.mu.RLock()
|
data, err := Cache.Get(ctx, key)
|
||||||
defer c.mu.RUnlock()
|
|
||||||
data, err := Cache.cache.Get([]byte(key))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return *new(T), err
|
||||||
}
|
}
|
||||||
dec := gob.NewDecoder(bytes.NewReader(data))
|
if v, ok := data.(T); ok {
|
||||||
err = dec.Decode(&value)
|
return v, nil
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return nil
|
return *new(T), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommonCache) Set(key string, value *types.File, expireSeconds int) error {
|
func CacheSet(ctx context.Context, key string, value any) error {
|
||||||
c.mu.Lock()
|
return Cache.Set(ctx, key, value)
|
||||||
defer c.mu.Unlock()
|
|
||||||
var buf bytes.Buffer
|
|
||||||
enc := gob.NewEncoder(&buf)
|
|
||||||
err := enc.Encode(value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
Cache.cache.Set([]byte(key), buf.Bytes(), expireSeconds)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommonCache) Delete(key string) error {
|
func CacheDelete(ctx context.Context, key string) error {
|
||||||
c.mu.Lock()
|
return Cache.Delete(ctx, key)
|
||||||
defer c.mu.Unlock()
|
|
||||||
Cache.cache.Del([]byte(key))
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
package logger
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
|
||||||
|
|
||||||
"github.com/gookit/slog"
|
"github.com/gookit/slog"
|
||||||
"github.com/gookit/slog/handler"
|
"github.com/gookit/slog/handler"
|
||||||
"github.com/gookit/slog/rotatefile"
|
"github.com/gookit/slog/rotatefile"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var L *slog.Logger
|
var Log *slog.Logger
|
||||||
|
|
||||||
func InitLogger() {
|
func InitLogger() {
|
||||||
if L != nil {
|
if Log != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
slog.DefaultChannelName = "SaveAnyBot"
|
slog.DefaultChannelName = "SaveAnyBot"
|
||||||
L = slog.New()
|
Log = slog.New()
|
||||||
logLevel := slog.LevelByName(config.Cfg.Log.Level)
|
logLevel := slog.LevelByName(config.Cfg.Log.Level)
|
||||||
logFilePath := config.Cfg.Log.File
|
logFilePath := config.Cfg.Log.File
|
||||||
logBackupNum := config.Cfg.Log.BackupCount
|
logBackupNum := config.Cfg.Log.BackupCount
|
||||||
@@ -26,15 +25,16 @@ func InitLogger() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
consoleH := handler.NewConsoleHandler(logLevels)
|
consoleH := handler.NewConsoleHandler(logLevels)
|
||||||
fileH, err := handler.NewTimeRotateFile(
|
Log.AddHandler(consoleH)
|
||||||
logFilePath,
|
if logFilePath != "" && logBackupNum > 0 {
|
||||||
rotatefile.EveryDay,
|
fileH, err := handler.NewTimeRotateFile(
|
||||||
handler.WithLogLevels(slog.AllLevels),
|
logFilePath,
|
||||||
handler.WithBackupNum(logBackupNum),
|
rotatefile.EveryDay,
|
||||||
handler.WithBuffSize(0),
|
handler.WithLogLevels(slog.AllLevels),
|
||||||
)
|
handler.WithBackupNum(logBackupNum))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
}
|
||||||
|
Log.AddHandler(fileH)
|
||||||
}
|
}
|
||||||
L.AddHandlers(consoleH, fileH)
|
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 创建文件, 自动创建目录
|
// 创建文件, 自动创建目录
|
||||||
@@ -31,10 +29,10 @@ func PurgeFile(path string) error {
|
|||||||
func RmFileAfter(path string, td time.Duration) {
|
func RmFileAfter(path string, td time.Duration) {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("Failed to create timer for %s: %s", path, err)
|
Log.Errorf("Failed to create timer for %s: %s", path, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.L.Debugf("Remove file after %s: %s", td, path)
|
Log.Debugf("Remove file after %s: %s", td, path)
|
||||||
time.AfterFunc(td, func() {
|
time.AfterFunc(td, func() {
|
||||||
PurgeFile(path)
|
PurgeFile(path)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package common
|
|||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HashString(s string) string {
|
func HashString(s string) string {
|
||||||
@@ -10,3 +11,16 @@ func HashString(s string) string {
|
|||||||
hash.Write([]byte(s))
|
hash.Write([]byte(s))
|
||||||
return hex.EncodeToString(hash.Sum(nil))
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var TagRe = regexp.MustCompile(`(?:^|[\p{Zs}\s.,!?(){}[\]<>\"\',。!?():;、])#([\p{L}\d_]+)`)
|
||||||
|
|
||||||
|
func ExtractTagsFromText(text string) []string {
|
||||||
|
matches := TagRe.FindAllStringSubmatch(text, -1)
|
||||||
|
tags := make([]string, 0)
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) > 1 {
|
||||||
|
tags = append(tags, match[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,74 +1,92 @@
|
|||||||
|
#创建文件时,若需要保留中文注释,请务必确保本文件编码为 UTF-8 ,否则会无法读取。
|
||||||
workers = 4 # 同时下载文件数
|
workers = 4 # 同时下载文件数
|
||||||
retry = 3 # 下载失败重试次数
|
retry = 3 # 下载失败重试次数
|
||||||
|
threads = 4 # 单个任务下载最大线程数
|
||||||
|
stream = false # 使用stream模式, 详情请查看文档
|
||||||
|
|
||||||
[telegram]
|
[telegram]
|
||||||
# Bot Token
|
# Bot Token
|
||||||
|
# 更换 Bot Token 后请删除数据库文件和 session.db
|
||||||
token = ""
|
token = ""
|
||||||
# Telegram API 配置, 若不配置也可运行, 将使用默认的 API ID 和 API HASH
|
# Telegram API 配置, 若不配置也可运行, 将使用默认的 API ID 和 API HASH
|
||||||
# 推荐使用自己的 API ID 和 API HASH (https://my.telegram.org)
|
# 推荐使用自己的 API ID 和 API HASH (https://my.telegram.org)
|
||||||
# app_id = 123456
|
# app_id = 1025907
|
||||||
# app_hash = "0123456789abcdef0123456789abcdef"
|
# app_hash = "452b0359b988148995f22ff0f4229750"
|
||||||
|
|
||||||
|
# 初始化超时时间, 单位: 秒
|
||||||
|
timeout = 60
|
||||||
|
|
||||||
|
# flood_retry = 5
|
||||||
|
# rpc_retry = 5
|
||||||
|
|
||||||
[telegram.proxy]
|
[telegram.proxy]
|
||||||
# 启用代理连接 telegram, 只支持 socks5
|
# 启用代理连接 telegram, 只支持 socks5
|
||||||
enable = false
|
enable = false
|
||||||
url = "socks5://127.0.0.1:7890"
|
url = "socks5://127.0.0.1:7890"
|
||||||
|
|
||||||
|
# 用户列表
|
||||||
|
[[users]]
|
||||||
|
# telegram user id
|
||||||
|
id = 114514
|
||||||
|
# 使用黑名单模式,开启后下方留空以使用所有存储,反之则为白名单,白名单请在下方输入允许的存储名
|
||||||
|
blacklist = true
|
||||||
|
# 将列表留空并开启黑名单模式以允许使用所有存储,此处示例为黑名单模式,用户 114514 可使用所有存储
|
||||||
|
storages = []
|
||||||
|
|
||||||
# 存储配置列表
|
[[users]]
|
||||||
|
id = 123456
|
||||||
|
blacklist = false # 使用白名单模式,此时,用户123456 仅可使用下方列表中的存储
|
||||||
|
# 此时该用户只能使用名为 本机1 的存储
|
||||||
|
storages = ["本机1"]
|
||||||
|
|
||||||
|
# 存储列表
|
||||||
[[storages]]
|
[[storages]]
|
||||||
# 标识名, 需要唯一
|
# 标识名, 需要唯一
|
||||||
name = "本机1"
|
name = "本机1"
|
||||||
# 存储类型, 目前可用: local , alist , webdav
|
# 存储类型, 目前可用: local, alist, webdav, minio
|
||||||
type = "local"
|
type = "local"
|
||||||
|
# 启用存储
|
||||||
enable = true
|
enable = true
|
||||||
|
# 文件保存根路径
|
||||||
base_path = "./downloads"
|
base_path = "./downloads"
|
||||||
|
|
||||||
[[storages]]
|
|
||||||
name = "本机2"
|
|
||||||
type = "local"
|
|
||||||
enable = true
|
|
||||||
base_path = "./downloads/2"
|
|
||||||
|
|
||||||
[[storages]]
|
[[storages]]
|
||||||
name = "MyAlist"
|
name = "MyAlist"
|
||||||
type = "alist"
|
type = "alist"
|
||||||
enable = true
|
enable = false #记得启用
|
||||||
base_path = '/'
|
base_path = '/'
|
||||||
url = 'https://alist.com'
|
url = 'https://alist.com'
|
||||||
username = 'admin'
|
username = 'admin'
|
||||||
password = 'password'
|
password = 'password'
|
||||||
token_exp = 86400
|
# alist token 刷新时间
|
||||||
|
# 86400--1天 604800--7天 1296000--15天 2592000--30天 15552000--180天
|
||||||
|
token_exp = 86400
|
||||||
# alist 可直接使用 token 登录, 此时 username, password, token_exp 将被忽略
|
# alist 可直接使用 token 登录, 此时 username, password, token_exp 将被忽略
|
||||||
# 请自行在 alist 侧配置合理的 token 过期时间
|
# 请自行在 alist 侧配置合理的 token 过期时间
|
||||||
# token = ""
|
# token = ""
|
||||||
|
|
||||||
|
|
||||||
[[storages]]
|
[[storages]]
|
||||||
name = "MyWebdav"
|
name = "MyWebdav"
|
||||||
type = "webdav"
|
type = "webdav"
|
||||||
|
enable = false
|
||||||
base_path = '/path/telegram'
|
base_path = '/path/telegram'
|
||||||
enable = true
|
|
||||||
url = 'https://example.com/dav'
|
url = 'https://example.com/dav'
|
||||||
username = 'username'
|
username = 'username'
|
||||||
password = 'password'
|
password = 'password'
|
||||||
|
|
||||||
|
[[storages]]
|
||||||
|
name = "MyMinio"
|
||||||
|
type = "minio"
|
||||||
|
enable = true
|
||||||
|
endpoint = 'play.min.io'
|
||||||
|
use_ssl = true
|
||||||
|
access_key_id = 'Q3AM3UQ867SPQQA43P2F'
|
||||||
|
secret_access_key = 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG'
|
||||||
|
bucket_name = 'saveanybot'
|
||||||
|
base_path = '/path/telegram'
|
||||||
|
|
||||||
# 用户列表
|
|
||||||
[[users]]
|
|
||||||
# user id
|
|
||||||
id = 123456
|
|
||||||
# 存储名称过滤列表
|
|
||||||
storages = ["MyAlist", "本机1"]
|
|
||||||
# 开启黑名单模式, 过滤列表中的存储将无法使用, 默认为白名单模式
|
|
||||||
blacklist = false
|
|
||||||
|
|
||||||
[[users]]
|
|
||||||
id = 114514
|
|
||||||
# 将列表留空并开启黑名单模式以允许使用所有存储
|
|
||||||
storages = []
|
|
||||||
blacklist = true
|
|
||||||
|
|
||||||
|
# 其他配置
|
||||||
|
|
||||||
# [log]
|
# [log]
|
||||||
# # 日志等级
|
# # 日志等级
|
||||||
@@ -82,3 +100,5 @@ blacklist = true
|
|||||||
|
|
||||||
# [db]
|
# [db]
|
||||||
# path = "data/data.db" # 数据库文件路径
|
# path = "data/data.db" # 数据库文件路径
|
||||||
|
# session = "data/session.db"
|
||||||
|
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
config/storage/alist.go
Normal file
38
config/storage/alist.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlistStorageConfig struct {
|
||||||
|
BaseConfig
|
||||||
|
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
|
||||||
|
}
|
||||||
63
config/storage/factory.go
Normal file
63
config/storage/factory.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var storageFactories = map[types.StorageType]func(cfg *BaseConfig) (StorageConfig, error){
|
||||||
|
types.StorageTypeLocal: createStorageConfig(&LocalStorageConfig{}),
|
||||||
|
types.StorageTypeAlist: createStorageConfig(&AlistStorageConfig{}),
|
||||||
|
types.StorageTypeWebdav: createStorageConfig(&WebdavStorageConfig{}),
|
||||||
|
types.StorageTypeMinio: createStorageConfig(&MinioStorageConfig{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
func createStorageConfig(configType StorageConfig) func(cfg *BaseConfig) (StorageConfig, error) {
|
||||||
|
return func(cfg *BaseConfig) (StorageConfig, error) {
|
||||||
|
configValue := reflect.New(reflect.TypeOf(configType).Elem()).Interface().(StorageConfig)
|
||||||
|
|
||||||
|
reflect.ValueOf(configValue).Elem().FieldByName("BaseConfig").Set(reflect.ValueOf(*cfg))
|
||||||
|
|
||||||
|
if err := mapstructure.Decode(cfg.RawConfig, configValue); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode %s storage config: %w", cfg.Type, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configValue, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadStorageConfigs(v *viper.Viper) ([]StorageConfig, error) {
|
||||||
|
var baseConfigs []BaseConfig
|
||||||
|
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[types.StorageType(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
|
||||||
|
}
|
||||||
27
config/storage/local.go
Normal file
27
config/storage/local.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalStorageConfig struct {
|
||||||
|
BaseConfig
|
||||||
|
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
|
||||||
|
}
|
||||||
41
config/storage/minio.go
Normal file
41
config/storage/minio.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MinioStorageConfig struct {
|
||||||
|
BaseConfig
|
||||||
|
Endpoint string `toml:"endpoint" mapstructure:"endpoint" json:"endpoint"`
|
||||||
|
AccessKeyID string `toml:"access_key_id" mapstructure:"access_key_id" json:"access_key_id"`
|
||||||
|
SecretAccessKey string `toml:"secret_access_key" mapstructure:"secret_access_key" json:"secret_access_key"`
|
||||||
|
BucketName string `toml:"bucket_name" mapstructure:"bucket_name" json:"bucket_name"`
|
||||||
|
UseSSL bool `toml:"use_ssl" mapstructure:"use_ssl" json:"use_ssl"`
|
||||||
|
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MinioStorageConfig) Validate() error {
|
||||||
|
if m.Endpoint == "" {
|
||||||
|
return fmt.Errorf("endpoint is required for minio storage")
|
||||||
|
}
|
||||||
|
if m.AccessKeyID == "" || m.SecretAccessKey == "" {
|
||||||
|
return fmt.Errorf("access_key_id and secret_access_key are required for minio storage")
|
||||||
|
}
|
||||||
|
if m.BucketName == "" {
|
||||||
|
return fmt.Errorf("bucket_name is required for minio storage")
|
||||||
|
}
|
||||||
|
if m.BasePath == "" {
|
||||||
|
return fmt.Errorf("base_path is required for minio storage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MinioStorageConfig) GetType() types.StorageType {
|
||||||
|
return types.StorageTypeMinio
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MinioStorageConfig) GetName() string {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
16
config/storage/types.go
Normal file
16
config/storage/types.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import "github.com/krau/SaveAny-Bot/types"
|
||||||
|
|
||||||
|
type StorageConfig interface {
|
||||||
|
Validate() error
|
||||||
|
GetType() types.StorageType
|
||||||
|
GetName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseConfig 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]any `toml:"-" mapstructure:",remain"`
|
||||||
|
}
|
||||||
36
config/storage/webdav.go
Normal file
36
config/storage/webdav.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebdavStorageConfig struct {
|
||||||
|
BaseConfig
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -10,40 +10,26 @@ type userConfig struct {
|
|||||||
Blacklist bool `toml:"blacklist" mapstructure:"blacklist" json:"blacklist"` // 黑名单模式, storage names 中的存储将不会被使用, 默认为白名单模式
|
Blacklist bool `toml:"blacklist" mapstructure:"blacklist" json:"blacklist"` // 黑名单模式, storage names 中的存储将不会被使用, 默认为白名单模式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userIDs []int64
|
||||||
|
var storages []string
|
||||||
|
var userStorages = make(map[int64][]string)
|
||||||
|
|
||||||
func (c *Config) GetStorageNamesByUserID(userID int64) []string {
|
func (c *Config) GetStorageNamesByUserID(userID int64) []string {
|
||||||
for _, user := range c.Users {
|
us, ok := userStorages[userID]
|
||||||
if user.ID == userID {
|
if ok {
|
||||||
if user.Blacklist {
|
return us
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetUsersID() []int64 {
|
func (c *Config) GetUsersID() []int64 {
|
||||||
var ids []int64
|
return userIDs
|
||||||
for _, user := range c.Users {
|
|
||||||
ids = append(ids, user.ID)
|
|
||||||
}
|
|
||||||
return ids
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) HasStorage(userID int64, storageName string) bool {
|
func (c *Config) HasStorage(userID int64, storageName string) bool {
|
||||||
for _, user := range c.Users {
|
us, ok := userStorages[userID]
|
||||||
if user.ID == userID {
|
if !ok {
|
||||||
if user.Blacklist {
|
return false
|
||||||
return !slice.Contain(user.Storages, storageName)
|
|
||||||
} else {
|
|
||||||
return slice.Contain(user.Storages, storageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
return slice.Contain(us, storageName)
|
||||||
}
|
}
|
||||||
|
|||||||
102
config/viper.go
102
config/viper.go
@@ -5,22 +5,25 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
|
"github.com/krau/SaveAny-Bot/config/storage"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
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"`
|
NoCleanCache bool `toml:"no_clean_cache" mapstructure:"no_clean_cache" json:"no_clean_cache"`
|
||||||
Users []userConfig `toml:"users" mapstructure:"users" json:"users"`
|
Threads int `toml:"threads" mapstructure:"threads" json:"threads"`
|
||||||
|
Stream bool `toml:"stream" mapstructure:"stream" json:"stream"`
|
||||||
|
|
||||||
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
Users []userConfig `toml:"users" mapstructure:"users" json:"users"`
|
||||||
Log logConfig `toml:"log" mapstructure:"log"`
|
|
||||||
DB dbConfig `toml:"db" mapstructure:"db"`
|
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
||||||
Telegram telegramConfig `toml:"telegram" mapstructure:"telegram"`
|
Log logConfig `toml:"log" mapstructure:"log"`
|
||||||
Storages []StorageConfig `toml:"-" mapstructure:"-" json:"storages"`
|
DB dbConfig `toml:"db" mapstructure:"db"`
|
||||||
// Deprecated
|
Telegram telegramConfig `toml:"telegram" mapstructure:"telegram"`
|
||||||
DeprecatedStorage deprecatedStorageConfig `toml:"storage" mapstructure:"storage"`
|
Storages []storage.StorageConfig `toml:"-" mapstructure:"-" json:"storages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type tempConfig struct {
|
type tempConfig struct {
|
||||||
@@ -35,17 +38,19 @@ type logConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type dbConfig struct {
|
type dbConfig struct {
|
||||||
Path string `toml:"path" mapstructure:"path"`
|
Path string `toml:"path" mapstructure:"path"`
|
||||||
|
Session string `toml:"session" mapstructure:"session"`
|
||||||
|
Expire int64 `toml:"expire" mapstructure:"expire"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type telegramConfig struct {
|
type telegramConfig struct {
|
||||||
Token string `toml:"token" mapstructure:"token"`
|
Token string `toml:"token" mapstructure:"token"`
|
||||||
AppID int `toml:"app_id" mapstructure:"app_id" json:"app_id"`
|
AppID int `toml:"app_id" mapstructure:"app_id" json:"app_id"`
|
||||||
AppHash string `toml:"app_hash" mapstructure:"app_hash" json:"app_hash"`
|
AppHash string `toml:"app_hash" mapstructure:"app_hash" json:"app_hash"`
|
||||||
Proxy proxyConfig `toml:"proxy" mapstructure:"proxy"`
|
Timeout int `toml:"timeout" mapstructure:"timeout" json:"timeout"`
|
||||||
|
Proxy proxyConfig `toml:"proxy" mapstructure:"proxy"`
|
||||||
// Deprecated
|
FloodRetry int `toml:"flood_retry" mapstructure:"flood_retry" json:"flood_retry"`
|
||||||
Admins []int64 `toml:"admins" mapstructure:"admins"`
|
RpcRetry int `toml:"rpc_retry" mapstructure:"rpc_retry" json:"rpc_retry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type proxyConfig struct {
|
type proxyConfig struct {
|
||||||
@@ -55,6 +60,15 @@ type proxyConfig struct {
|
|||||||
|
|
||||||
var Cfg *Config
|
var Cfg *Config
|
||||||
|
|
||||||
|
func (c Config) GetStorageByName(name string) storage.StorageConfig {
|
||||||
|
for _, storage := range c.Storages {
|
||||||
|
if storage.GetName() == name {
|
||||||
|
return storage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func Init() error {
|
func Init() error {
|
||||||
viper.SetConfigName("config")
|
viper.SetConfigName("config")
|
||||||
viper.AddConfigPath(".")
|
viper.AddConfigPath(".")
|
||||||
@@ -67,18 +81,22 @@ func Init() error {
|
|||||||
|
|
||||||
viper.SetDefault("workers", 3)
|
viper.SetDefault("workers", 3)
|
||||||
viper.SetDefault("retry", 3)
|
viper.SetDefault("retry", 3)
|
||||||
|
viper.SetDefault("threads", 4)
|
||||||
|
|
||||||
viper.SetDefault("telegram.app_id", 1025907)
|
viper.SetDefault("telegram.app_id", 1025907)
|
||||||
viper.SetDefault("telegram.app_hash", "452b0359b988148995f22ff0f4229750")
|
viper.SetDefault("telegram.app_hash", "452b0359b988148995f22ff0f4229750")
|
||||||
|
viper.SetDefault("telegram.timeout", 60)
|
||||||
|
viper.SetDefault("telegram.flood_retry", 5)
|
||||||
|
viper.SetDefault("telegram.rpc_retry", 5)
|
||||||
|
|
||||||
viper.SetDefault("temp.base_path", "cache/")
|
viper.SetDefault("temp.base_path", "cache/")
|
||||||
viper.SetDefault("temp.cache_ttl", 3600)
|
viper.SetDefault("temp.cache_ttl", 30)
|
||||||
|
|
||||||
viper.SetDefault("log.level", "INFO")
|
viper.SetDefault("log.level", "INFO")
|
||||||
viper.SetDefault("log.file", "logs/saveany.log")
|
|
||||||
viper.SetDefault("log.backup_count", 7)
|
|
||||||
|
|
||||||
viper.SetDefault("db.path", "data/saveany.db")
|
viper.SetDefault("db.path", "data/saveany.db")
|
||||||
|
viper.SetDefault("db.session", "data/session.db")
|
||||||
|
viper.SetDefault("db.expire", 86400*5)
|
||||||
|
|
||||||
if err := viper.SafeWriteConfigAs("config.toml"); err != nil {
|
if err := viper.SafeWriteConfigAs("config.toml"); err != nil {
|
||||||
if _, ok := err.(viper.ConfigFileAlreadyExistsError); !ok {
|
if _, ok := err.(viper.ConfigFileAlreadyExistsError); !ok {
|
||||||
@@ -98,38 +116,12 @@ func Init() error {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if Cfg.Telegram.Admins != nil {
|
storagesConfig, err := storage.LoadStorageConfigs(viper.GetViper())
|
||||||
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error loading storage configs: %w", err)
|
return fmt.Errorf("error loading storage configs: %w", err)
|
||||||
}
|
}
|
||||||
Cfg.Storages = storagesConfig
|
Cfg.Storages = storagesConfig
|
||||||
|
|
||||||
if Cfg.DeprecatedStorage != (deprecatedStorageConfig{}) {
|
|
||||||
fmt.Println("\n警告: 你正在使用旧版存储配置, 未来版本将会被废弃.\n请参考新的配置文件模板.")
|
|
||||||
transformDeprecatedStorageConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
storageNames := make(map[string]struct{})
|
storageNames := make(map[string]struct{})
|
||||||
for _, storage := range Cfg.Storages {
|
for _, storage := range Cfg.Storages {
|
||||||
if _, ok := storageNames[storage.GetName()]; ok {
|
if _, ok := storageNames[storage.GetName()]; ok {
|
||||||
@@ -147,6 +139,18 @@ func Init() error {
|
|||||||
return fmt.Errorf("workers 和 retry 必须大于 0, 当前值: workers=%d, retry=%d", Cfg.Workers, Cfg.Retry)
|
return fmt.Errorf("workers 和 retry 必须大于 0, 当前值: workers=%d, retry=%d", Cfg.Workers, Cfg.Retry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, storage := range Cfg.Storages {
|
||||||
|
storages = append(storages, storage.GetName())
|
||||||
|
}
|
||||||
|
for _, user := range Cfg.Users {
|
||||||
|
userIDs = append(userIDs, user.ID)
|
||||||
|
if user.Blacklist {
|
||||||
|
userStorages[user.ID] = slice.Compact(slice.Difference(storages, user.Storages))
|
||||||
|
} else {
|
||||||
|
userStorages[user.ID] = user.Storages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
160
core/core.go
160
core/core.go
@@ -4,131 +4,37 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"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/telegram/downloader"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/bot"
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"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 {
|
var Downloader *downloader.Downloader
|
||||||
logger.L.Debugf("Start processing task: %s", task.String())
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if task.StoragePath == "" {
|
func init() {
|
||||||
task.StoragePath = task.File.FileName
|
Downloader = downloader.NewDownloader().WithPartSize(1024 * 1024)
|
||||||
}
|
|
||||||
|
|
||||||
taskStorage, err := storage.GetStorageByUserIDAndName(task.UserID, task.StorageName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
progressCallback := func(bytesRead, contentLength int64) {
|
|
||||||
progress := float64(bytesRead) / float64(contentLength) * 100
|
|
||||||
logger.L.Tracef("Downloading %s: %.2f%%", task.String(), progress)
|
|
||||||
if task.File.FileSize < 1024*1024*50 || int(progress)%(100/barTotalCount) != 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
text, entities := buildProgressMessageEntity(task, barTotalCount, bytesRead, task.StartTime, progress)
|
|
||||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
|
||||||
Message: text,
|
|
||||||
Entities: entities,
|
|
||||||
ID: task.ReplyMessageID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
text, entities := buildProgressMessageEntity(task, barTotalCount, 0, task.StartTime, 0)
|
|
||||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
|
||||||
Message: text,
|
|
||||||
Entities: entities,
|
|
||||||
ID: task.ReplyMessageID,
|
|
||||||
})
|
|
||||||
|
|
||||||
readCloser, err := NewTelegramReader(task.Ctx, bot.Client, &task.File.Location,
|
|
||||||
0, task.File.FileSize-1, task.File.FileSize,
|
|
||||||
progressCallback, task.File.FileSize/100)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("创建下载失败: %w", err)
|
|
||||||
}
|
|
||||||
defer readCloser.Close()
|
|
||||||
|
|
||||||
dest, err := os.Create(cacheDestPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("创建文件失败: %w", err)
|
|
||||||
}
|
|
||||||
defer dest.Close()
|
|
||||||
task.StartTime = time.Now()
|
|
||||||
if _, err := io.CopyN(dest, readCloser, task.File.FileSize); err != nil {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.L.Infof("Downloaded file: %s", cacheDestPath)
|
|
||||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
|
||||||
Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()),
|
|
||||||
ID: task.ReplyMessageID,
|
|
||||||
})
|
|
||||||
|
|
||||||
return saveFileWithRetry(task, taskStorage, cacheDestPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
||||||
for {
|
for {
|
||||||
semaphore <- struct{}{}
|
semaphore <- struct{}{}
|
||||||
task := queue.GetTask()
|
task := queue.GetTask()
|
||||||
logger.L.Debugf("Got task: %s", task.String())
|
common.Log.Debugf("Got task: %s", task.String())
|
||||||
|
|
||||||
switch task.Status {
|
switch task.Status {
|
||||||
case types.Pending:
|
case types.Pending:
|
||||||
logger.L.Infof("Processing task: %s", task.String())
|
common.Log.Infof("Processing task: %s", task.String())
|
||||||
if err := processPendingTask(&task); err != nil {
|
if err := processPendingTask(task); err != nil {
|
||||||
logger.L.Errorf("Failed to do task: %s", err)
|
|
||||||
task.Error = err
|
task.Error = err
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
logger.L.Debugf("Task canceled: %s", task.String())
|
|
||||||
task.Status = types.Canceled
|
task.Status = types.Canceled
|
||||||
} else {
|
} else {
|
||||||
|
common.Log.Errorf("Failed to do task: %s", err)
|
||||||
task.Status = types.Failed
|
task.Status = types.Failed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -136,29 +42,49 @@ 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())
|
common.Log.Infof("Task succeeded: %s", task.String())
|
||||||
task.Ctx.(*ext.Context).EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
extCtx, ok := task.Ctx.(*ext.Context)
|
||||||
Message: fmt.Sprintf("文件保存成功\n [%s]: %s", task.StorageName, task.StoragePath),
|
if !ok {
|
||||||
ID: task.ReplyMessageID,
|
common.Log.Errorf("Context is not *ext.Context: %T", task.Ctx)
|
||||||
})
|
} else if task.ReplyMessageID != 0 {
|
||||||
|
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("文件保存成功\n [%s]: %s", task.StorageName, task.StoragePath),
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
})
|
||||||
|
}
|
||||||
case types.Failed:
|
case types.Failed:
|
||||||
logger.L.Errorf("Task failed: %s", task.String())
|
common.Log.Errorf("Task failed: %s", task.String())
|
||||||
task.Ctx.(*ext.Context).EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
extCtx, ok := task.Ctx.(*ext.Context)
|
||||||
Message: "文件保存失败\n" + task.Error.Error(),
|
if !ok {
|
||||||
ID: task.ReplyMessageID,
|
common.Log.Errorf("Context is not *ext.Context: %T", task.Ctx)
|
||||||
})
|
} else if task.ReplyMessageID != 0 {
|
||||||
|
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: "文件保存失败\n" + task.Error.Error(),
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
})
|
||||||
|
}
|
||||||
case types.Canceled:
|
case types.Canceled:
|
||||||
logger.L.Infof("Task canceled: %s", task.String())
|
common.Log.Infof("Task canceled: %s", task.String())
|
||||||
|
extCtx, ok := task.Ctx.(*ext.Context)
|
||||||
|
if !ok {
|
||||||
|
common.Log.Errorf("Context is not *ext.Context: %T", task.Ctx)
|
||||||
|
} else if task.ReplyMessageID != 0 {
|
||||||
|
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: "任务已取消",
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
})
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
logger.L.Errorf("Unknown task status: %s", task.Status)
|
common.Log.Errorf("Unknown task status: %s", task.Status)
|
||||||
}
|
}
|
||||||
<-semaphore
|
<-semaphore
|
||||||
logger.L.Debugf("Task done: %s", task.String())
|
common.Log.Debugf("Task done: %s; status: %s", task.String(), task.Status)
|
||||||
|
queue.DoneTask(task)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run() {
|
func Run() {
|
||||||
logger.L.Info("Start processing tasks...")
|
common.Log.Info("Start processing tasks...")
|
||||||
semaphore := make(chan struct{}, config.Cfg.Workers)
|
semaphore := make(chan struct{}, config.Cfg.Workers)
|
||||||
for i := 0; i < config.Cfg.Workers; i++ {
|
for i := 0; i < config.Cfg.Workers; i++ {
|
||||||
go worker(queue.Queue, semaphore)
|
go worker(queue.Queue, semaphore)
|
||||||
|
|||||||
285
core/download.go
Normal file
285
core/download.go
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/celestix/telegraph-go/v2"
|
||||||
|
"github.com/duke-git/lancet/v2/fileutil"
|
||||||
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/bot"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func processPendingTask(task *types.Task) error {
|
||||||
|
common.Log.Debugf("Start processing task: %s", task.String())
|
||||||
|
if task.FileName() == "" {
|
||||||
|
task.File.FileName = fmt.Sprintf("%d_%d_%s", task.FileChatID, task.FileMessageID, task.File.Hash())
|
||||||
|
}
|
||||||
|
|
||||||
|
taskStorage, storagePath, err := getStorageAndPathForTask(task)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if taskStorage == nil {
|
||||||
|
return fmt.Errorf("not found storage: %s", task.StorageName)
|
||||||
|
}
|
||||||
|
task.StoragePath = storagePath
|
||||||
|
|
||||||
|
ctx, ok := task.Ctx.(*ext.Context)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("context is not *ext.Context: %T", task.Ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelCtx, cancel := context.WithCancel(ctx)
|
||||||
|
task.Cancel = cancel
|
||||||
|
|
||||||
|
if task.IsTelegraph {
|
||||||
|
return processTelegraph(ctx, cancelCtx, task, taskStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.File.FileSize == 0 {
|
||||||
|
return processPhoto(task, taskStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadBuilder := Downloader.Download(bot.Client.API(), task.File.Location).WithThreads(getTaskThreads(task.File.FileSize))
|
||||||
|
|
||||||
|
notsupportStreamStorage, notsupportStream := taskStorage.(storage.StorageNotSupportStream)
|
||||||
|
cancelMarkUp := getCancelTaskMarkup(task)
|
||||||
|
if config.Cfg.Stream {
|
||||||
|
if !notsupportStream {
|
||||||
|
text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0)
|
||||||
|
if task.ReplyMessageID != 0 {
|
||||||
|
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: text,
|
||||||
|
Entities: entities,
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
ReplyMarkup: cancelMarkUp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
defer pr.Close()
|
||||||
|
|
||||||
|
task.StartTime = time.Now()
|
||||||
|
progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize))
|
||||||
|
|
||||||
|
progressStream := NewProgressStream(pw, task.File.FileSize, progressCallback)
|
||||||
|
|
||||||
|
eg, uploadCtx := errgroup.WithContext(cancelCtx)
|
||||||
|
|
||||||
|
eg.Go(func() error {
|
||||||
|
return taskStorage.Save(uploadCtx, pr, task.StoragePath)
|
||||||
|
})
|
||||||
|
eg.Go(func() error {
|
||||||
|
_, err := downloadBuilder.Stream(uploadCtx, progressStream)
|
||||||
|
if closeErr := pw.CloseWithError(err); closeErr != nil {
|
||||||
|
common.Log.Errorf("Failed to close pipe writer: %v", closeErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err := eg.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
common.Log.Warnf("存储 %s 不支持流式传输: %s", task.StorageName, notsupportStreamStorage.NotSupportStream())
|
||||||
|
|
||||||
|
if task.ReplyMessageID != 0 {
|
||||||
|
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("存储 %s 不支持流式传输: %s\n正在使用普通下载...", task.StorageName, notsupportStreamStorage.NotSupportStream()),
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
ReplyMarkup: cancelMarkUp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0)
|
||||||
|
if task.ReplyMessageID != 0 {
|
||||||
|
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: text,
|
||||||
|
Entities: entities,
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
ReplyMarkup: cancelMarkUp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize))
|
||||||
|
dest, err := NewTaskLocalFile(cacheDestPath, task.File.FileSize, progressCallback)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建文件失败: %w", err)
|
||||||
|
}
|
||||||
|
defer dest.Close()
|
||||||
|
task.StartTime = time.Now()
|
||||||
|
_, err = downloadBuilder.Parallel(cancelCtx, dest)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("下载文件失败: %w", err)
|
||||||
|
}
|
||||||
|
defer cleanCacheFile(cacheDestPath)
|
||||||
|
|
||||||
|
fixTaskFileExt(task, cacheDestPath)
|
||||||
|
|
||||||
|
common.Log.Infof("Downloaded file: %s", cacheDestPath)
|
||||||
|
if task.ReplyMessageID != 0 {
|
||||||
|
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()),
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return saveFileWithRetry(cancelCtx, task.StoragePath, taskStorage, cacheDestPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processTelegraph(extCtx *ext.Context, cancelCtx context.Context, task *types.Task, taskStorage storage.Storage) error {
|
||||||
|
if bot.TelegraphClient == nil {
|
||||||
|
return fmt.Errorf("telegraph client is not initialized")
|
||||||
|
}
|
||||||
|
tgphUrl := task.TelegraphURL
|
||||||
|
tgphPath := strings.Split(tgphUrl, "/")[len(strings.Split(tgphUrl, "/"))-1]
|
||||||
|
if tgphUrl == "" || tgphPath == "" {
|
||||||
|
return fmt.Errorf("invalid telegraph url")
|
||||||
|
}
|
||||||
|
entityBuilder := entity.Builder{}
|
||||||
|
text := fmt.Sprintf("正在下载 Telegraph \n文件夹: %s\n保存路径: %s",
|
||||||
|
task.FileName(),
|
||||||
|
fmt.Sprintf("[%s]:%s", task.StorageName, task.StoragePath),
|
||||||
|
)
|
||||||
|
var entities []tg.MessageEntityClass
|
||||||
|
if err := styling.Perform(&entityBuilder,
|
||||||
|
styling.Plain("正在下载 Telegraph \n文件夹: "),
|
||||||
|
styling.Code(task.FileName()),
|
||||||
|
styling.Plain("\n保存路径: "),
|
||||||
|
styling.Code(fmt.Sprintf("[%s]:%s", task.StorageName, task.StoragePath)),
|
||||||
|
); err != nil {
|
||||||
|
common.Log.Errorf("Failed to build entities: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.ReplyMessageID != 0 {
|
||||||
|
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: text,
|
||||||
|
Entities: entities,
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
ReplyMarkup: getCancelTaskMarkup(task),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCh := make(chan error)
|
||||||
|
go func() {
|
||||||
|
page, err := bot.TelegraphClient.GetPage(tgphPath, true)
|
||||||
|
if err != nil {
|
||||||
|
resultCh <- fmt.Errorf("获取 telegraph 页面失败: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imgs := make([]string, 0)
|
||||||
|
for _, element := range page.Content {
|
||||||
|
var node telegraph.NodeElement
|
||||||
|
data, err := json.Marshal(element)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("Failed to marshal element: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &node)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("Failed to unmarshal element: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(node.Children) != 0 {
|
||||||
|
for _, child := range node.Children {
|
||||||
|
imgs = append(imgs, getNodeImages(child)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Tag == "img" {
|
||||||
|
if src, ok := node.Attrs["src"]; ok {
|
||||||
|
imgs = append(imgs, src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if len(imgs) == 0 {
|
||||||
|
resultCh <- fmt.Errorf("没有找到图片")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hc := bot.TelegraphClient.HttpClient
|
||||||
|
eg, ectx := errgroup.WithContext(cancelCtx)
|
||||||
|
eg.SetLimit(config.Cfg.Workers) // TODO: use a new config field for this
|
||||||
|
for i, img := range imgs {
|
||||||
|
if strings.HasPrefix(img, "/file/") {
|
||||||
|
img = "https://telegra.ph" + img
|
||||||
|
}
|
||||||
|
eg.Go(func() error {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := range config.Cfg.Retry {
|
||||||
|
if attempt > 0 {
|
||||||
|
retryDelay := time.Duration(attempt*attempt) * time.Second
|
||||||
|
select {
|
||||||
|
case <-ectx.Done():
|
||||||
|
return ectx.Err()
|
||||||
|
case <-time.After(retryDelay):
|
||||||
|
}
|
||||||
|
common.Log.Debugf("Retrying to download image %s (attempt %d)", img, attempt+1)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ectx, http.MethodGet, img, nil)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp, err := hc.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = fmt.Errorf("发送请求失败: %w", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
lastErr = fmt.Errorf("请求图片失败: %s", resp.Status)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targetPath := path.Join(task.StoragePath, fmt.Sprintf("%d%s", i+1, path.Ext(img)))
|
||||||
|
err = taskStorage.Save(ectx, resp.Body, targetPath)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = fmt.Errorf("保存图片失败: %w", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
common.Log.Infof("Saved image: %s", targetPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return lastErr
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := eg.Wait(); err != nil {
|
||||||
|
resultCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resultCh <- nil
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case err := <-resultCh:
|
||||||
|
return err
|
||||||
|
case <-cancelCtx.Done():
|
||||||
|
return cancelCtx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
80
core/download_test.go
Normal file
80
core/download_test.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/celestix/telegraph-go/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetImgSrcs(t *testing.T) {
|
||||||
|
complexStructure := telegraph.NodeElement{
|
||||||
|
Tag: "div",
|
||||||
|
Children: []telegraph.Node{
|
||||||
|
telegraph.NodeElement{
|
||||||
|
Tag: "figure",
|
||||||
|
Children: []telegraph.Node{
|
||||||
|
telegraph.NodeElement{
|
||||||
|
Tag: "img",
|
||||||
|
Attrs: map[string]string{
|
||||||
|
"src": "https://example.com/image1.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
telegraph.NodeElement{
|
||||||
|
Tag: "p",
|
||||||
|
Children: []telegraph.Node{
|
||||||
|
"A text node",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
telegraph.NodeElement{
|
||||||
|
Tag: "figure",
|
||||||
|
Children: []telegraph.Node{
|
||||||
|
telegraph.NodeElement{
|
||||||
|
Tag: "img",
|
||||||
|
Attrs: map[string]string{
|
||||||
|
"src": "https://example.com/image2.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
telegraph.NodeElement{
|
||||||
|
Tag: "img",
|
||||||
|
Attrs: map[string]string{
|
||||||
|
"src": "https://example.com/image3.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"text node",
|
||||||
|
telegraph.NodeElement{
|
||||||
|
Tag: "div",
|
||||||
|
Children: []telegraph.Node{
|
||||||
|
telegraph.NodeElement{
|
||||||
|
Tag: "span",
|
||||||
|
Children: []telegraph.Node{
|
||||||
|
telegraph.NodeElement{
|
||||||
|
Tag: "img",
|
||||||
|
Attrs: map[string]string{
|
||||||
|
"src": "https://example.com/image4.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"https://example.com/image1.png",
|
||||||
|
"https://example.com/image2.png",
|
||||||
|
"https://example.com/image3.png",
|
||||||
|
"https://example.com/image4.png",
|
||||||
|
}
|
||||||
|
|
||||||
|
got := getNodeImages(complexStructure)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(expected, got) {
|
||||||
|
t.Errorf("expected %v,got %v", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
154
core/reader.go
154
core/reader.go
@@ -1,154 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/celestix/gotgproto"
|
|
||||||
"github.com/gotd/td/tg"
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type telegramReader struct {
|
|
||||||
client *gotgproto.Client
|
|
||||||
location *tg.InputFileLocationClass
|
|
||||||
bytesread int64
|
|
||||||
chunkSize int64
|
|
||||||
i int64
|
|
||||||
contentLength int64
|
|
||||||
start int64
|
|
||||||
end int64
|
|
||||||
next func() ([]byte, error)
|
|
||||||
progressCallback func(bytesRead, contentLength int64)
|
|
||||||
callbackInterval int64
|
|
||||||
lastProgress int64
|
|
||||||
buffer []byte
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*telegramReader) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *telegramReader) Read(p []byte) (n int, err error) {
|
|
||||||
if r.bytesread == r.contentLength {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.i >= int64(len(r.buffer)) {
|
|
||||||
r.buffer, err = r.next()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if len(r.buffer) == 0 {
|
|
||||||
r.next = r.partStream()
|
|
||||||
r.buffer, err = r.next()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
r.i = 0
|
|
||||||
}
|
|
||||||
n = copy(p, r.buffer[r.i:])
|
|
||||||
r.i += int64(n)
|
|
||||||
r.bytesread += int64(n)
|
|
||||||
|
|
||||||
if r.progressCallback != nil && (r.bytesread-r.lastProgress >= r.callbackInterval || r.bytesread == r.contentLength) {
|
|
||||||
r.progressCallback(r.bytesread, r.contentLength)
|
|
||||||
r.lastProgress = r.bytesread
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTelegramReader(
|
|
||||||
ctx context.Context,
|
|
||||||
client *gotgproto.Client,
|
|
||||||
location *tg.InputFileLocationClass,
|
|
||||||
start int64,
|
|
||||||
end int64,
|
|
||||||
contentLength int64,
|
|
||||||
progressCallback func(bytesRead, contentLength int64),
|
|
||||||
callbackInterval int64,
|
|
||||||
) (io.ReadCloser, error) {
|
|
||||||
|
|
||||||
r := &telegramReader{
|
|
||||||
ctx: ctx,
|
|
||||||
location: location,
|
|
||||||
client: client,
|
|
||||||
start: start,
|
|
||||||
end: end,
|
|
||||||
chunkSize: int64(1024 * 1024),
|
|
||||||
contentLength: contentLength,
|
|
||||||
progressCallback: progressCallback,
|
|
||||||
callbackInterval: callbackInterval,
|
|
||||||
}
|
|
||||||
|
|
||||||
r.next = r.partStream()
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *telegramReader) chunk(offset int64, limit int64) ([]byte, error) {
|
|
||||||
var lastError error
|
|
||||||
for i := 0; i < config.Cfg.Retry; i++ {
|
|
||||||
req := &tg.UploadGetFileRequest{
|
|
||||||
Offset: offset,
|
|
||||||
Limit: int(limit),
|
|
||||||
Location: *r.location,
|
|
||||||
}
|
|
||||||
res, err := r.client.API().UploadGetFile(r.ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), tg.ErrTimeout) {
|
|
||||||
lastError = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch result := res.(type) {
|
|
||||||
case *tg.UploadFile:
|
|
||||||
return result.Bytes, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected type %T", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, lastError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *telegramReader) partStream() func() ([]byte, error) {
|
|
||||||
|
|
||||||
start := r.start
|
|
||||||
end := r.end
|
|
||||||
offset := start - (start % r.chunkSize)
|
|
||||||
|
|
||||||
firstPartCut := start - offset
|
|
||||||
lastPartCut := (end % r.chunkSize) + 1
|
|
||||||
partCount := int((end - offset + r.chunkSize) / r.chunkSize)
|
|
||||||
currentPart := 1
|
|
||||||
|
|
||||||
readData := func() ([]byte, error) {
|
|
||||||
if currentPart > partCount {
|
|
||||||
return make([]byte, 0), nil
|
|
||||||
}
|
|
||||||
res, err := r.chunk(offset, r.chunkSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(res) == 0 {
|
|
||||||
return res, nil
|
|
||||||
} else if partCount == 1 {
|
|
||||||
res = res[firstPartCut:lastPartCut]
|
|
||||||
} else if currentPart == 1 {
|
|
||||||
res = res[firstPartCut:]
|
|
||||||
} else if currentPart == partCount {
|
|
||||||
res = res[:lastPartCut]
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPart++
|
|
||||||
offset += r.chunkSize
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
return readData
|
|
||||||
}
|
|
||||||
103
core/rule.go
Normal file
103
core/rule.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/krau/SaveAny-Bot/bot"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/dao"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStorageAndPathForTask(task *types.Task) (storage.Storage, string, error) {
|
||||||
|
user, err := dao.GetUserByChatID(task.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to get user by chat ID: %w", err)
|
||||||
|
}
|
||||||
|
if task.StoragePath == "" {
|
||||||
|
task.StoragePath = task.FileName()
|
||||||
|
}
|
||||||
|
taskStorage, err := storage.GetStorageByUserIDAndName(task.UserID, task.StorageName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
storagePath := taskStorage.JoinStoragePath(*task)
|
||||||
|
if !user.ApplyRule || user.Rules == nil {
|
||||||
|
return taskStorage, storagePath, nil
|
||||||
|
}
|
||||||
|
var ruleTaskStorage storage.Storage
|
||||||
|
var ruleStoragePath string
|
||||||
|
for _, rule := range user.Rules {
|
||||||
|
matchStorage, matchStoragePath := applyRule(&rule, *task)
|
||||||
|
if matchStorage != nil && matchStoragePath != "" {
|
||||||
|
ruleTaskStorage = matchStorage
|
||||||
|
ruleStoragePath = matchStoragePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ruleStoragePath == "" || ruleTaskStorage == nil {
|
||||||
|
return taskStorage, storagePath, nil
|
||||||
|
}
|
||||||
|
common.Log.Debugf("Rule matched: %s, %s", ruleTaskStorage.Name(), ruleStoragePath)
|
||||||
|
return ruleTaskStorage, ruleStoragePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyRule(rule *dao.Rule, task types.Task) (storage.Storage, string) {
|
||||||
|
var DirPath, StorageName string
|
||||||
|
switch rule.Type {
|
||||||
|
case string(types.RuleTypeFileNameRegex):
|
||||||
|
ruleRegex, err := regexp.Compile(rule.Data)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("failed to compile regex: %s", err)
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
if !ruleRegex.MatchString(task.FileName()) {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
DirPath = rule.DirPath
|
||||||
|
StorageName = rule.StorageName
|
||||||
|
case string(types.RuleTypeMessageRegex):
|
||||||
|
ruleRegex, err := regexp.Compile(rule.Data)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("failed to compile regex: %s", err)
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
ctx, ok := task.Ctx.(*ext.Context)
|
||||||
|
if !ok {
|
||||||
|
common.Log.Fatalf("context is not *ext.Context: %T", task.Ctx)
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
msg, err := bot.GetTGMessage(ctx, task.FileChatID, task.FileMessageID)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("failed to get message: %s", err)
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
if msg == nil {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
if !ruleRegex.MatchString(msg.GetMessage()) {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
DirPath = rule.DirPath
|
||||||
|
StorageName = rule.StorageName
|
||||||
|
default:
|
||||||
|
common.Log.Errorf("unknown rule type: %s", rule.Type)
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
taskStorageName := func() string {
|
||||||
|
if StorageName == "" || StorageName == "CHOSEN" {
|
||||||
|
return task.StorageName
|
||||||
|
}
|
||||||
|
return StorageName
|
||||||
|
}()
|
||||||
|
taskStorage, err := storage.GetStorageByUserIDAndName(task.UserID, taskStorageName)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.Errorf("failed to get storage: %s", err)
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
task.StoragePath = path.Join(DirPath, task.StoragePath)
|
||||||
|
return taskStorage, taskStorage.JoinStoragePath(task)
|
||||||
|
}
|
||||||
249
core/utils.go
249
core/utils.go
@@ -1,28 +1,58 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/celestix/telegraph-go/v2"
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/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"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func saveFileWithRetry(task *types.Task, taskStorage storage.Storage, localFilePath string) error {
|
func saveFileWithRetry(ctx context.Context, storagePath string, taskStorage storage.Storage, cacheFilePath string) error {
|
||||||
|
file, err := os.Open(cacheFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open cache file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
fileStat, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get file stat: %w", err)
|
||||||
|
}
|
||||||
|
vctx := context.WithValue(ctx, types.ContextKeyContentLength, fileStat.Size())
|
||||||
for i := 0; i <= config.Cfg.Retry; i++ {
|
for i := 0; i <= config.Cfg.Retry; i++ {
|
||||||
if err := taskStorage.Save(task.Ctx, localFilePath, task.StoragePath); err != nil {
|
if err := vctx.Err(); err != nil {
|
||||||
|
return fmt.Errorf("context canceled while saving file: %w", err)
|
||||||
|
}
|
||||||
|
file, err := os.Open(cacheFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open cache file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
if err := taskStorage.Save(vctx, file, 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)
|
common.Log.Errorf("Failed to save file: %s, retrying...", err)
|
||||||
|
select {
|
||||||
|
case <-vctx.Done():
|
||||||
|
return fmt.Errorf("context canceled during retry delay: %w", vctx.Err())
|
||||||
|
case <-time.After(time.Duration(i*500) * time.Millisecond):
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -30,7 +60,7 @@ func saveFileWithRetry(task *types.Task, taskStorage storage.Storage, localFileP
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processPhoto(task *types.Task, taskStorage storage.Storage, cachePath string) error {
|
func processPhoto(task *types.Task, taskStorage storage.Storage) error {
|
||||||
res, err := bot.Client.API().UploadGetFile(task.Ctx, &tg.UploadGetFileRequest{
|
res, err := bot.Client.API().UploadGetFile(task.Ctx, &tg.UploadGetFileRequest{
|
||||||
Location: task.File.Location,
|
Location: task.File.Location,
|
||||||
Offset: 0,
|
Offset: 0,
|
||||||
@@ -45,28 +75,9 @@ func processPhoto(task *types.Task, taskStorage storage.Storage, cachePath strin
|
|||||||
return fmt.Errorf("unexpected type %T", res)
|
return fmt.Errorf("unexpected type %T", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(cachePath, result.Bytes, os.ModePerm); err != nil {
|
common.Log.Infof("Downloaded photo: %s", task.FileName())
|
||||||
return fmt.Errorf("failed to write file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer cleanCacheFile(cachePath)
|
return taskStorage.Save(task.Ctx, bytes.NewReader(result.Bytes), task.StoragePath)
|
||||||
|
|
||||||
logger.L.Infof("Downloaded file: %s", cachePath)
|
|
||||||
|
|
||||||
return saveFileWithRetry(task, taskStorage, cachePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getProgressBar(progress float64, totalCount int) string {
|
|
||||||
bar := ""
|
|
||||||
barSize := 100 / totalCount
|
|
||||||
for i := 0; i < totalCount; i++ {
|
|
||||||
if int(progress)/barSize > i {
|
|
||||||
bar += "█"
|
|
||||||
} else {
|
|
||||||
bar += "░"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bar
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanCacheFile(destPath string) {
|
func cleanCacheFile(destPath string) {
|
||||||
@@ -74,21 +85,22 @@ func cleanCacheFile(destPath string) {
|
|||||||
common.RmFileAfter(destPath, time.Duration(config.Cfg.Temp.CacheTTL)*time.Second)
|
common.RmFileAfter(destPath, time.Duration(config.Cfg.Temp.CacheTTL)*time.Second)
|
||||||
} else {
|
} else {
|
||||||
if err := os.Remove(destPath); err != nil {
|
if err := os.Remove(destPath); err != nil {
|
||||||
logger.L.Errorf("Failed to purge file: %s", err)
|
common.Log.Errorf("Failed to purge file: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateBarTotalCount(fileSize int64) int {
|
// 获取进度需要更新的次数
|
||||||
barTotalCount := 5
|
func getProgressUpdateCount(fileSize int64) int {
|
||||||
|
updateCount := 5
|
||||||
if fileSize > 1024*1024*1000 {
|
if fileSize > 1024*1024*1000 {
|
||||||
barTotalCount = 40
|
updateCount = 50
|
||||||
} else if fileSize > 1024*1024*500 {
|
} else if fileSize > 1024*1024*500 {
|
||||||
barTotalCount = 20
|
updateCount = 20
|
||||||
} else if fileSize > 1024*1024*200 {
|
} else if fileSize > 1024*1024*200 {
|
||||||
barTotalCount = 10
|
updateCount = 10
|
||||||
}
|
}
|
||||||
return barTotalCount
|
return updateCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSpeed(bytesRead int64, startTime time.Time) string {
|
func getSpeed(bytesRead int64, startTime time.Time) string {
|
||||||
@@ -100,13 +112,12 @@ func getSpeed(bytesRead int64, startTime time.Time) string {
|
|||||||
return fmt.Sprintf("%.2fMB/s", speed)
|
return fmt.Sprintf("%.2fMB/s", speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildProgressMessageEntity(task *types.Task, barTotalCount int, bytesRead int64, startTime time.Time, progress float64) (string, []tg.MessageEntityClass) {
|
func buildProgressMessageEntity(task *types.Task, bytesRead int64, startTime time.Time, progress float64) (string, []tg.MessageEntityClass) {
|
||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
text := fmt.Sprintf("正在处理下载任务\n文件名: %s\n保存路径: %s\n平均速度: %s\n当前进度: [%s] %.2f%%",
|
text := fmt.Sprintf("正在处理下载任务\n文件名: %s\n保存路径: %s\n平均速度: %s\n当前进度: %.2f%%",
|
||||||
task.FileName(),
|
task.FileName(),
|
||||||
fmt.Sprintf("[%s]:%s", task.StorageName, task.StoragePath),
|
fmt.Sprintf("[%s]:%s", task.StorageName, task.StoragePath),
|
||||||
getSpeed(bytesRead, startTime),
|
getSpeed(bytesRead, startTime),
|
||||||
getProgressBar(progress, barTotalCount),
|
|
||||||
progress,
|
progress,
|
||||||
)
|
)
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
@@ -117,11 +128,171 @@ func buildProgressMessageEntity(task *types.Task, barTotalCount int, bytesRead i
|
|||||||
styling.Code(fmt.Sprintf("[%s]:%s", task.StorageName, task.StoragePath)),
|
styling.Code(fmt.Sprintf("[%s]:%s", task.StorageName, task.StoragePath)),
|
||||||
styling.Plain("\n平均速度: "),
|
styling.Plain("\n平均速度: "),
|
||||||
styling.Bold(getSpeed(bytesRead, task.StartTime)),
|
styling.Bold(getSpeed(bytesRead, task.StartTime)),
|
||||||
styling.Plain("\n当前进度:\n "),
|
styling.Plain("\n当前进度: "),
|
||||||
styling.Code(fmt.Sprintf("[%s] %.2f%%", getProgressBar(progress, barTotalCount), progress)),
|
styling.Bold(fmt.Sprintf("%.2f%%", progress)),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.L.Errorf("Failed to build entities: %s", err)
|
common.Log.Errorf("Failed to build entities: %s", err)
|
||||||
return text, entities
|
return text, entities
|
||||||
}
|
}
|
||||||
return entityBuilder.Complete()
|
return entityBuilder.Complete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildProgressCallback(ctx *ext.Context, task *types.Task, updateCount int) func(bytesRead, contentLength int64) {
|
||||||
|
return func(bytesRead, contentLength int64) {
|
||||||
|
progress := float64(bytesRead) / float64(contentLength) * 100
|
||||||
|
common.Log.Tracef("Downloading %s: %.2f%%", task.String(), progress)
|
||||||
|
progressInt := int(progress)
|
||||||
|
if task.File.FileSize < 1024*1024*50 || progressInt == 0 || progressInt%int(100/updateCount) != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if task.ReplyMessageID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text, entities := buildProgressMessageEntity(task, bytesRead, task.StartTime, progress)
|
||||||
|
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: text,
|
||||||
|
Entities: entities,
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
ReplyMarkup: getCancelTaskMarkup(task),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCancelTaskMarkup(task *types.Task) *tg.ReplyInlineMarkup {
|
||||||
|
return &tg.ReplyInlineMarkup{
|
||||||
|
Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonCallback{Text: "取消任务", Data: fmt.Appendf(nil, "cancel %s", task.Key())}}}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixTaskFileExt(task *types.Task, localFilePath string) {
|
||||||
|
if path.Ext(task.FileName()) == "" {
|
||||||
|
mimeType, err := mimetype.DetectFile(localFilePath)
|
||||||
|
if err != nil {
|
||||||
|
common.Log.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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTaskThreads(fileSize int64) int {
|
||||||
|
threads := 1
|
||||||
|
if fileSize > 1024*1024*100 {
|
||||||
|
threads = config.Cfg.Threads
|
||||||
|
} else if fileSize > 1024*1024*50 {
|
||||||
|
threads = config.Cfg.Threads / 2
|
||||||
|
}
|
||||||
|
return threads
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskLocalFile struct {
|
||||||
|
file *os.File
|
||||||
|
size int64
|
||||||
|
done int64
|
||||||
|
progressCallback func(bytesRead, contentLength int64)
|
||||||
|
callbackTimes int64
|
||||||
|
nextCallbackAt int64
|
||||||
|
callbackInterval int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TaskLocalFile) Read(p []byte) (n int, err error) {
|
||||||
|
return t.file.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TaskLocalFile) Close() error {
|
||||||
|
return t.file.Close()
|
||||||
|
}
|
||||||
|
func (t *TaskLocalFile) WriteAt(p []byte, off int64) (int, error) {
|
||||||
|
n, err := t.file.WriteAt(p, off)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
t.done += int64(n)
|
||||||
|
if t.progressCallback != nil && t.done >= t.nextCallbackAt {
|
||||||
|
t.progressCallback(t.done, t.size)
|
||||||
|
t.nextCallbackAt += t.callbackInterval
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTaskLocalFile(filePath string, fileSize int64, progressCallback func(bytesRead, contentLength int64)) (*TaskLocalFile, error) {
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||||
|
}
|
||||||
|
var callbackInterval int64
|
||||||
|
callbackInterval = fileSize / 100
|
||||||
|
if callbackInterval == 0 {
|
||||||
|
callbackInterval = 1
|
||||||
|
}
|
||||||
|
return &TaskLocalFile{
|
||||||
|
file: file,
|
||||||
|
size: fileSize,
|
||||||
|
progressCallback: progressCallback,
|
||||||
|
callbackTimes: 100,
|
||||||
|
nextCallbackAt: callbackInterval,
|
||||||
|
callbackInterval: callbackInterval,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgressStream struct {
|
||||||
|
writer io.Writer
|
||||||
|
size int64
|
||||||
|
done int64
|
||||||
|
callback func(bytesRead, contentLength int64)
|
||||||
|
nextAt int64
|
||||||
|
interval int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *ProgressStream) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = ps.writer.Write(p)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
ps.done += int64(n)
|
||||||
|
if ps.callback != nil && ps.done >= ps.nextAt {
|
||||||
|
ps.callback(ps.done, ps.size)
|
||||||
|
ps.nextAt += ps.interval
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProgressStream(writer io.Writer, size int64, callback func(bytesRead, contentLength int64)) *ProgressStream {
|
||||||
|
var interval int64
|
||||||
|
interval = size / 100
|
||||||
|
if interval == 0 {
|
||||||
|
interval = 1
|
||||||
|
}
|
||||||
|
return &ProgressStream{
|
||||||
|
writer: writer,
|
||||||
|
size: size,
|
||||||
|
callback: callback,
|
||||||
|
nextAt: interval,
|
||||||
|
interval: interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNodeImages(node telegraph.Node) []string {
|
||||||
|
var srcs []string
|
||||||
|
|
||||||
|
var nodeElement telegraph.NodeElement
|
||||||
|
data, err := json.Marshal(node)
|
||||||
|
if err != nil {
|
||||||
|
return srcs
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &nodeElement)
|
||||||
|
if err != nil {
|
||||||
|
return srcs
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodeElement.Tag == "img" {
|
||||||
|
if src, exists := nodeElement.Attrs["src"]; exists {
|
||||||
|
srcs = append(srcs, src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, child := range nodeElement.Children {
|
||||||
|
srcs = append(srcs, getNodeImages(child)...)
|
||||||
|
}
|
||||||
|
return srcs
|
||||||
|
}
|
||||||
|
|||||||
19
dao/callback_data.go
Normal file
19
dao/callback_data.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
func CreateCallbackData(data string) (uint, error) {
|
||||||
|
callbackData := CallbackData{
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
err := db.Create(&callbackData).Error
|
||||||
|
return callbackData.ID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCallbackData(id uint) (string, error) {
|
||||||
|
var callbackData CallbackData
|
||||||
|
err := db.First(&callbackData, id).Error
|
||||||
|
return callbackData.Data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteCallbackData(id uint) error {
|
||||||
|
return db.Unscoped().Where("id = ?", id).Delete(&CallbackData{}).Error
|
||||||
|
}
|
||||||
61
dao/db.go
61
dao/db.go
@@ -1,15 +1,15 @@
|
|||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
|
"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/types"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
glogger "gorm.io/gorm/logger"
|
glogger "gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
@@ -18,12 +18,11 @@ var db *gorm.DB
|
|||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
if err := os.MkdirAll(filepath.Dir(config.Cfg.DB.Path), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(config.Cfg.DB.Path), 0755); err != nil {
|
||||||
logger.L.Fatal("Failed to create data directory: ", err)
|
common.Log.Panic("Failed to create data directory: ", err)
|
||||||
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{
|
Logger: glogger.New(common.Log, glogger.Config{
|
||||||
Colorful: true,
|
Colorful: true,
|
||||||
SlowThreshold: time.Second * 5,
|
SlowThreshold: time.Second * 5,
|
||||||
LogLevel: glogger.Error,
|
LogLevel: glogger.Error,
|
||||||
@@ -33,17 +32,25 @@ func Init() {
|
|||||||
PrepareStmt: true,
|
PrepareStmt: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Fatal("Failed to open database: ", err)
|
common.Log.Panic("Failed to open database: ", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
logger.L.Debug("Database connected")
|
common.Log.Debug("Database connected")
|
||||||
if err := db.AutoMigrate(&types.ReceivedFile{}, &types.User{}); err != nil {
|
if err := db.AutoMigrate(&ReceivedFile{}, &User{}, &Dir{}, &CallbackData{}, &Rule{}); err != nil {
|
||||||
logger.L.Fatal("迁移数据库失败, 如果您从旧版本升级, 建议手动删除数据库文件后重试: ", err)
|
common.Log.Panic("迁移数据库失败, 如果您从旧版本升级, 建议手动删除数据库文件后重试: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syncUsers(); err != nil {
|
if err := syncUsers(); err != nil {
|
||||||
logger.L.Fatal("Failed to sync users:", err)
|
common.Log.Panic("Failed to sync users:", err)
|
||||||
}
|
}
|
||||||
|
common.Log.Debug("Database migrated")
|
||||||
|
if config.Cfg.DB.Expire == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := cleanExpiredData(db); err != nil {
|
||||||
|
common.Log.Error("Failed to clean expired data: ", err)
|
||||||
|
} else {
|
||||||
|
common.Log.Debug("Cleaned expired data")
|
||||||
|
}
|
||||||
|
go cleanJob(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncUsers() error {
|
func syncUsers() error {
|
||||||
@@ -52,7 +59,7 @@ func syncUsers() error {
|
|||||||
return fmt.Errorf("failed to get users: %w", err)
|
return fmt.Errorf("failed to get users: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dbUserMap := make(map[int64]types.User)
|
dbUserMap := make(map[int64]User)
|
||||||
for _, u := range dbUsers {
|
for _, u := range dbUsers {
|
||||||
dbUserMap[u.ChatID] = u
|
dbUserMap[u.ChatID] = u
|
||||||
}
|
}
|
||||||
@@ -67,7 +74,7 @@ func syncUsers() error {
|
|||||||
if err := CreateUser(cfgID); err != nil {
|
if err := CreateUser(cfgID); err != nil {
|
||||||
return fmt.Errorf("failed to create user %d: %w", cfgID, err)
|
return fmt.Errorf("failed to create user %d: %w", cfgID, err)
|
||||||
}
|
}
|
||||||
logger.L.Infof("创建用户: %d", cfgID)
|
common.Log.Infof("创建用户: %d", cfgID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,9 +83,33 @@ func syncUsers() error {
|
|||||||
if err := DeleteUser(&dbUser); err != nil {
|
if err := DeleteUser(&dbUser); err != nil {
|
||||||
return fmt.Errorf("failed to delete user %d: %w", dbID, err)
|
return fmt.Errorf("failed to delete user %d: %w", dbID, err)
|
||||||
}
|
}
|
||||||
logger.L.Infof("删除用户: %d", dbID)
|
common.Log.Infof("删除用户: %d", dbID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanExpiredData(db *gorm.DB) error {
|
||||||
|
var fileErr error
|
||||||
|
if err := db.Where("updated_at < ?", time.Now().Add(-time.Duration(config.Cfg.DB.Expire)*time.Second)).Unscoped().Delete(&ReceivedFile{}).Error; err != nil {
|
||||||
|
fileErr = fmt.Errorf("failed to delete expired files: %w", err)
|
||||||
|
}
|
||||||
|
var cbErr error
|
||||||
|
if err := db.Where("updated_at < ?", time.Now().Add(-time.Duration(config.Cfg.DB.Expire)*time.Second)).Unscoped().Delete(&CallbackData{}).Error; err != nil {
|
||||||
|
cbErr = fmt.Errorf("failed to delete expired callback data: %w", err)
|
||||||
|
}
|
||||||
|
return errors.Join(fileErr, cbErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanJob(db *gorm.DB) {
|
||||||
|
tick := time.NewTicker(time.Duration(config.Cfg.DB.Expire) * time.Second)
|
||||||
|
defer tick.Stop()
|
||||||
|
for range tick.C {
|
||||||
|
if err := cleanExpiredData(db); err != nil {
|
||||||
|
common.Log.Error("Failed to clean expired data: ", err)
|
||||||
|
} else {
|
||||||
|
common.Log.Debug("Cleaned expired data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
47
dao/dir.go
Normal file
47
dao/dir.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
func CreateDirForUser(userID uint, storageName, path string) error {
|
||||||
|
dir := Dir{
|
||||||
|
UserID: userID,
|
||||||
|
StorageName: storageName,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
return db.Create(&dir).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDirByID(id uint) (*Dir, error) {
|
||||||
|
dir := &Dir{}
|
||||||
|
err := db.First(dir, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dir, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserDirs(userID uint) ([]Dir, error) {
|
||||||
|
var dirs []Dir
|
||||||
|
err := db.Where("user_id = ?", userID).Find(&dirs).Error
|
||||||
|
return dirs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserDirsByChatID(chatID int64) ([]Dir, error) {
|
||||||
|
user, err := GetUserByChatID(chatID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return GetUserDirs(user.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDirsByUserIDAndStorageName(userID uint, storageName string) ([]Dir, error) {
|
||||||
|
var dirs []Dir
|
||||||
|
err := db.Where("user_id = ? AND storage_name = ?", userID, storageName).Find(&dirs).Error
|
||||||
|
return dirs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteDirForUser(userID uint, storageName, path string) error {
|
||||||
|
return db.Unscoped().Where("user_id = ? AND storage_name = ? AND path = ?", userID, storageName, path).Delete(&Dir{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteDirByID(id uint) error {
|
||||||
|
return db.Unscoped().Delete(&Dir{}, id).Error
|
||||||
|
}
|
||||||
10
dao/file.go
10
dao/file.go
@@ -1,8 +1,6 @@
|
|||||||
package dao
|
package dao
|
||||||
|
|
||||||
import "github.com/krau/SaveAny-Bot/types"
|
func SaveReceivedFile(receivedFile *ReceivedFile) error {
|
||||||
|
|
||||||
func SaveReceivedFile(receivedFile *types.ReceivedFile) error {
|
|
||||||
record, err := GetReceivedFileByChatAndMessageID(receivedFile.ChatID, receivedFile.MessageID)
|
record, err := GetReceivedFileByChatAndMessageID(receivedFile.ChatID, receivedFile.MessageID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
receivedFile.ID = record.ID
|
receivedFile.ID = record.ID
|
||||||
@@ -10,8 +8,8 @@ func SaveReceivedFile(receivedFile *types.ReceivedFile) error {
|
|||||||
return db.Save(receivedFile).Error
|
return db.Save(receivedFile).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*types.ReceivedFile, error) {
|
func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*ReceivedFile, error) {
|
||||||
var receivedFile types.ReceivedFile
|
var receivedFile ReceivedFile
|
||||||
err := db.Where("chat_id = ? AND message_id = ?", chatID, messageID).First(&receivedFile).Error
|
err := db.Where("chat_id = ? AND message_id = ?", chatID, messageID).First(&receivedFile).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -19,6 +17,6 @@ func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*types.Rece
|
|||||||
return &receivedFile, nil
|
return &receivedFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteReceivedFile(receivedFile *types.ReceivedFile) error {
|
func DeleteReceivedFile(receivedFile *ReceivedFile) error {
|
||||||
return db.Unscoped().Delete(receivedFile).Error
|
return db.Unscoped().Delete(receivedFile).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package types
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -14,6 +14,8 @@ type ReceivedFile struct {
|
|||||||
ReplyMessageID int
|
ReplyMessageID int
|
||||||
ReplyChatID int64
|
ReplyChatID int64
|
||||||
FileName string
|
FileName string
|
||||||
|
IsTelegraph bool
|
||||||
|
TelegraphURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -21,4 +23,28 @@ type User struct {
|
|||||||
ChatID int64 `gorm:"uniqueIndex;not null"`
|
ChatID int64 `gorm:"uniqueIndex;not null"`
|
||||||
Silent bool
|
Silent bool
|
||||||
DefaultStorage string // Default storage name
|
DefaultStorage string // Default storage name
|
||||||
|
Dirs []Dir
|
||||||
|
ApplyRule bool
|
||||||
|
Rules []Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dir struct {
|
||||||
|
gorm.Model
|
||||||
|
UserID uint
|
||||||
|
StorageName string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallbackData struct {
|
||||||
|
gorm.Model
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
gorm.Model
|
||||||
|
UserID uint
|
||||||
|
Type string
|
||||||
|
Data string
|
||||||
|
StorageName string
|
||||||
|
DirPath string
|
||||||
}
|
}
|
||||||
22
dao/rule.go
Normal file
22
dao/rule.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
func CreateRule(rule *Rule) error {
|
||||||
|
return db.Create(rule).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteRule(ruleID uint) error {
|
||||||
|
return db.Unscoped().Delete(&Rule{}, ruleID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateUserApplyRule(chatID int64, applyRule bool) error {
|
||||||
|
return db.Model(&User{}).Where("chat_id = ?", chatID).Update("apply_rule", applyRule).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRulesByUserChatID(chatID int64) ([]Rule, error) {
|
||||||
|
var rules []Rule
|
||||||
|
err := db.Where("user_id = (SELECT id FROM users WHERE chat_id = ?)", chatID).Find(&rules).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rules, nil
|
||||||
|
}
|
||||||
29
dao/user.go
29
dao/user.go
@@ -1,32 +1,33 @@
|
|||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreateUser(chatID int64) error {
|
func CreateUser(chatID int64) error {
|
||||||
if _, err := GetUserByChatID(chatID); err == nil {
|
if _, err := GetUserByChatID(chatID); err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return db.Create(&types.User{ChatID: chatID}).Error
|
return db.Create(&User{ChatID: chatID}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllUsers() ([]types.User, error) {
|
func GetAllUsers() ([]User, error) {
|
||||||
var users []types.User
|
var users []User
|
||||||
err := db.Find(&users).Error
|
err := db.Preload("Dirs").
|
||||||
|
Preload("Rules").
|
||||||
|
Find(&users).Error
|
||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserByChatID(chatID int64) (*types.User, error) {
|
func GetUserByChatID(chatID int64) (*User, error) {
|
||||||
var user types.User
|
var user User
|
||||||
err := db.Where("chat_id = ?", chatID).First(&user).Error
|
err := db.
|
||||||
|
Preload("Dirs").
|
||||||
|
Preload("Rules").
|
||||||
|
Where("chat_id = ?", chatID).First(&user).Error
|
||||||
return &user, err
|
return &user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateUser(user *types.User) error {
|
func UpdateUser(user *User) error {
|
||||||
return db.Save(user).Error
|
return db.Save(user).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteUser(user *types.User) error {
|
func DeleteUser(user *User) error {
|
||||||
return db.Unscoped().Delete(user).Error
|
return db.Unscoped().Select("Dirs", "Rules").Delete(user).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,7 @@ services:
|
|||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./config.toml:/app/config.toml
|
- ./config.toml:/app/config.toml
|
||||||
- ./downloads:/app/downloads
|
- ./downloads:/app/downloads
|
||||||
- ./cache:/app/cache
|
- ./cache:/app/cache
|
||||||
|
# 使用 host 模式以便访问宿主机服务 (如代理)
|
||||||
|
# 如果你对 Docker 网络模式熟悉, 可以自行修改
|
||||||
|
network_mode: host
|
||||||
11
docs/docs/contribute.md
Normal file
11
docs/docs/contribute.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# 参与开发
|
||||||
|
|
||||||
|
## 贡献新存储端
|
||||||
|
|
||||||
|
1. Fork 本项目, 克隆到本地
|
||||||
|
2. 在 `config/storage` 目录下定义存储端配置, 并添加到 `config/storage/factory.go` 中
|
||||||
|
3. 在 `types/types.go` 中添加新的存储端类型
|
||||||
|
4. 在 `storage` 目录下新建一个包, 编写存储端实现, 然后在 `storage/storage.go` 中导入并添加它
|
||||||
|
5. 更新 `config.example.toml` 文件, 添加新的示例配置
|
||||||
|
|
||||||
|
*可能确实有点麻烦了 = =*
|
||||||
94
docs/docs/deploy.md
Normal file
94
docs/docs/deploy.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# 部署指南
|
||||||
|
|
||||||
|
## 从二进制文件部署
|
||||||
|
|
||||||
|
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
|
||||||
|
|
||||||
|
在解压后目录新建 `config.toml` 文件, 参考 [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
||||||
|
|
||||||
|
运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x saveany-bot
|
||||||
|
./saveany-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加为 systemd 服务
|
||||||
|
|
||||||
|
创建文件 `/etc/systemd/system/saveany-bot.service` 并写入以下内容:
|
||||||
|
|
||||||
|
```
|
||||||
|
[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
|
||||||
|
```
|
||||||
|
|
||||||
|
设为开机启动并启动服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl enable --now saveany-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 为OpenWrt及衍生系统添加开机自启动服务
|
||||||
|
|
||||||
|
创建文件 ` /etc/init.d/saveanybot` ,参考[saveanybot](https://github.com/krau/SaveAny-Bot/blob/main/docs/saveanybot)自行修改.
|
||||||
|
|
||||||
|
`chmod +x /etc/init.d/saveanybot`
|
||||||
|
|
||||||
|
完成后,将文件复制到 `/etc/rc.d`并重命名为`S99saveanybot`.
|
||||||
|
|
||||||
|
`chmod +x /etc/rc.d/S99saveanybot`
|
||||||
|
|
||||||
|
### 为OpenWrt及衍生系统添加快捷指令
|
||||||
|
|
||||||
|
创建文件` /usr/bin/sabot` ,参考[sabot](https://github.com/krau/SaveAny-Bot/blob/main/docs/sabot)自行配置修改,注意此处文件编码仅支持 ANSI 936 .
|
||||||
|
|
||||||
|
`chmod +x /usr/bin/sabot`
|
||||||
|
|
||||||
|
之后,终端输入`sabot start|stop|restart|status|enable|disable`即可.
|
||||||
|
|
||||||
|
|
||||||
|
## 使用 Docker 部署
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
下载 [docker-compose.yml](https://github.com/krau/SaveAny-Bot/blob/main/docker-compose.yml) 文件, 在同目录下新建 `config.toml` 文件, 参考 [config.example.toml](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` 升级到最新版
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./saveany-bot upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
如果是 Docker 部署, 使用以下命令更新:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull ghcr.io/krau/saveany-bot:latest
|
||||||
|
docker restart saveany-bot
|
||||||
|
```
|
||||||
16
docs/docs/faq.md
Normal file
16
docs/docs/faq.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 常见问题
|
||||||
|
|
||||||
|
## 上传 alist 失败也会显示成功
|
||||||
|
|
||||||
|
在 alist 管理页面适当调整上传分片大小, 为 alist 使用更稳定的网络环境部署, 都可以减少这种情况的发生.
|
||||||
|
|
||||||
|
## Bot 提示下载成功但是 alist 未显示
|
||||||
|
|
||||||
|
alist 缓存了目录结构, 参考文档可以调整缓存时间
|
||||||
|
|
||||||
|
https://alist.nn.ci/zh/guide/drivers/common.html#缓存过期
|
||||||
|
|
||||||
|
## docker部署配置了代理后仍无法连接 telegram (初始化客户端超时)
|
||||||
|
|
||||||
|
docker 不能直接访问宿主机网络, 如果你不熟悉其用法, 请将容器设为 host 模式:
|
||||||
|
|
||||||
38
docs/docs/help.md
Normal file
38
docs/docs/help.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 使用帮助
|
||||||
|
|
||||||
|
## 保存文件
|
||||||
|
|
||||||
|
Bot 接受两种消息: 文件和链接.
|
||||||
|
|
||||||
|
支持以下链接:
|
||||||
|
|
||||||
|
1. 公开频道 (具有用户名) 的消息链接, 例如: `https://t.me/acherkrau/1097`. **即使频道禁止了转发和保存, Bot 依然可以下载其文件.**
|
||||||
|
2. Telegra.ph 的文章链接, Bot 将下载其中的所有图片
|
||||||
|
|
||||||
|
## 静默模式 (silent)
|
||||||
|
|
||||||
|
使用 `/silent` 命令可以开关静默模式.
|
||||||
|
|
||||||
|
默认情况下不开启静默模式, Bot 会询问你每个文件的保存位置.
|
||||||
|
|
||||||
|
开启静默模式后, Bot 会直接保存文件到默认位置, 无需确认.
|
||||||
|
|
||||||
|
在开启静默模式之前, 需要使用 `/storage` 命令设置默认保存位置.
|
||||||
|
|
||||||
|
## Stream 模式
|
||||||
|
|
||||||
|
在配置文件中将 `stream` 设置为 `true` 可以开启 Stream 模式.
|
||||||
|
|
||||||
|
未开启时, Bot 处理任务分为两步: 下载和上传. Bot 会将文件暂存到本地, 然后上传到对应存储位置, 最后删除本地文件.
|
||||||
|
|
||||||
|
开启后, Bot 将直接将文件流式传输到存储端, 不需要下载到本地.
|
||||||
|
|
||||||
|
该功能对于硬盘空间有限的部署环境十分有用, 然而相较于普通模式也具有一些弊端:
|
||||||
|
|
||||||
|
- 无法使用多线程从 telegram 下载文件, 速度较慢.
|
||||||
|
- 网络不稳定时, 任务失败率高.
|
||||||
|
- 无法在中间层对文件进行处理, 例如自动文件类型识别.
|
||||||
|
|
||||||
|
**不支持** Stream 模式的存储端:
|
||||||
|
|
||||||
|
- alist
|
||||||
7
docs/docs/index.md
Normal file
7
docs/docs/index.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# SaveAnyBot 文档
|
||||||
|
|
||||||
|
SaveAnyBot 是一个可以保存 Telegram 上的文件到云存储的机器人, 就像 PikPak Bot 一样.
|
||||||
|
|
||||||
|
不同的是, SaveAnyBot 提供更灵活的存储端选择, 并实现一些更强大的功能.
|
||||||
|
|
||||||
|
本项目以 AGPL-3.0 协议开源, 请遵守协议使用.
|
||||||
34
docs/mkdocs.yml
Normal file
34
docs/mkdocs.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
site_name: SaveAnyBot 官方文档
|
||||||
|
site_author: Krau
|
||||||
|
site_description: SaveAnyBot 是一个可以保存 Telegram 上的文件到多种云存储的机器人, 本文档将帮助你了解如何部署和使用它.
|
||||||
|
repo_name: krau/saveany-bot
|
||||||
|
repo_url: https://github.com/krau/saveany-bot
|
||||||
|
copyright: CC BY-NC-SA 4.0
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
language: zh
|
||||||
|
highlightjs: true
|
||||||
|
palette:
|
||||||
|
- media: "(prefers-color-scheme)"
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-auto
|
||||||
|
name: 切换主题
|
||||||
|
- media: "(prefers-color-scheme: light)"
|
||||||
|
scheme: default
|
||||||
|
primary: indigo
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-7
|
||||||
|
name: 暗色模式
|
||||||
|
- media: "(prefers-color-scheme: dark)"
|
||||||
|
scheme: slate
|
||||||
|
primary: blue grey
|
||||||
|
toggle:
|
||||||
|
icon: material/brightness-4
|
||||||
|
name: 亮色模式
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- index.md
|
||||||
|
- deploy.md
|
||||||
|
- help.md
|
||||||
|
- faq.md
|
||||||
|
- contribute.md
|
||||||
28
docs/sabot
Normal file
28
docs/sabot
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
/etc/init.d/saveanybot start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
/etc/init.d/saveanybot stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
/etc/init.d/saveanybot restart
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
/etc/init.d/saveanybot status
|
||||||
|
;;
|
||||||
|
enable)
|
||||||
|
/etc/init.d/saveanybot enable
|
||||||
|
echo "Enable SaveAnyBot auto-start."
|
||||||
|
;;
|
||||||
|
disable)
|
||||||
|
/etc/init.d/saveanybot disable
|
||||||
|
echo "Disable SaveAnyBot auto-start."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {start|stop|restart|status|enable|disable}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
34
docs/saveanybot
Normal file
34
docs/saveanybot
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
|
||||||
|
# This is the OpenWRT init.d script for SaveAnyBot
|
||||||
|
|
||||||
|
START=99 # 设置启动顺序,数字越大越后启动
|
||||||
|
STOP=10 # 设置停止顺序,数字越小越先停止
|
||||||
|
|
||||||
|
# 脚本描述
|
||||||
|
description="SaveAnyBot"
|
||||||
|
|
||||||
|
# 设置工作目录和执行文件路径
|
||||||
|
WORKING_DIR="/mnt/mmc1-1/SaveAnyBot"
|
||||||
|
EXEC_PATH="$WORKING_DIR/saveany-bot"
|
||||||
|
|
||||||
|
# 启动函数
|
||||||
|
start() {
|
||||||
|
echo "Starting SaveAnyBot..."
|
||||||
|
# 切换到工作目录并执行程序
|
||||||
|
cd $WORKING_DIR
|
||||||
|
$EXEC_PATH &
|
||||||
|
}
|
||||||
|
|
||||||
|
# 停止函数
|
||||||
|
stop() {
|
||||||
|
echo "Stopping SaveAnyBot..."
|
||||||
|
# 查找并杀死进程
|
||||||
|
killall saveany-bot
|
||||||
|
}
|
||||||
|
|
||||||
|
# 重启函数
|
||||||
|
reload() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
98
go.mod
98
go.mod
@@ -4,25 +4,28 @@ 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.2
|
github.com/celestix/gotgproto v1.0.0-beta21
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8
|
github.com/celestix/telegraph-go/v2 v2.0.4
|
||||||
github.com/gookit/slog v0.5.7
|
github.com/eko/gocache/lib/v4 v4.2.0
|
||||||
|
github.com/eko/gocache/store/go_cache/v4 v4.2.2
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9
|
||||||
|
github.com/gookit/slog v0.5.8
|
||||||
github.com/gotd/contrib v0.21.0
|
github.com/gotd/contrib v0.21.0
|
||||||
github.com/gotd/td v0.120.0
|
github.com/gotd/td v0.123.0
|
||||||
|
github.com/minio/minio-go/v7 v7.0.91
|
||||||
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.9.1
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.20.1
|
||||||
github.com/studio-b12/gowebdav v0.10.0
|
golang.org/x/net v0.40.0
|
||||||
golang.org/x/net v0.35.0
|
golang.org/x/time v0.11.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/beorn7/perks v1.0.1 // 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/coder/websocket v1.8.12 // indirect
|
github.com/coder/websocket v1.8.13 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // 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
|
||||||
@@ -32,74 +35,87 @@ require (
|
|||||||
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-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/golang/mock v1.6.0 // 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-20250317173921-a4b03ec1a45e // 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/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/klauspost/cpuid/v2 v2.2.10 // 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/minio/crc64nvme v1.0.2 // indirect
|
||||||
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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.10.0 // indirect
|
github.com/ogen-go/ogen v1.12.0 // indirect
|
||||||
github.com/onsi/gomega v1.36.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/prometheus/client_golang v1.22.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.63.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.16.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/rs/xid v1.6.0 // 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
|
||||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.2 // indirect
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
golang.org/x/crypto v0.33.0 // indirect
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
golang.org/x/mod v0.23.0 // indirect
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
golang.org/x/oauth2 v0.26.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/tools v0.30.0 // indirect
|
golang.org/x/term v0.32.0 // indirect
|
||||||
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gorm.io/driver/mysql v1.5.6 // indirect
|
modernc.org/libc v1.65.0 // 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.10.0 // indirect
|
||||||
modernc.org/sqlite v1.35.0 // indirect
|
modernc.org/sqlite v1.37.0 // indirect
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/coocood/freecache v1.2.4
|
github.com/duke-git/lancet/v2 v2.3.5
|
||||||
github.com/duke-git/lancet/v2 v2.3.4
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/gookit/color v1.5.4 // indirect
|
github.com/gookit/color v1.5.4 // indirect
|
||||||
github.com/gookit/goutil v0.6.18 // indirect
|
github.com/gookit/goutil v0.6.18 // indirect
|
||||||
github.com/gookit/gsr v0.1.0 // indirect
|
github.com/gookit/gsr v0.1.1 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.9 // indirect
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.12.0 // indirect
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
github.com/spf13/cast v1.7.1 // indirect
|
github.com/spf13/cast v1.8.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
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-20250210185358-939b2ce775ac // indirect
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/sync v0.14.0
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.25.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.26.0
|
||||||
gorm.io/gorm v1.25.12
|
|
||||||
)
|
)
|
||||||
|
|||||||
227
go.sum
227
go.sum
@@ -1,21 +1,25 @@
|
|||||||
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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
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.2 h1:+WcsKdsyj4xy+TAV+4Sw6zp1xiQrIr4dMnM31+k8NYM=
|
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/celestix/gotgproto v1.0.0-beta20.2/go.mod h1:j42ZhBMUke6QyBLvCgx8tA+TL9L3+pq/Q46B+b5+3aU=
|
||||||
|
github.com/celestix/gotgproto v1.0.0-beta21 h1:VUuAC/Kj5Sdu/WZan3ZUb0GFNAavFxMYxmHAhCBX0J8=
|
||||||
|
github.com/celestix/gotgproto v1.0.0-beta21/go.mod h1:viDkHe9rBegJoEE/jNuFfbBM0XZ3pSx/ugjaNaVnbvU=
|
||||||
|
github.com/celestix/telegraph-go/v2 v2.0.4 h1:w8HWymJFhMSMPjdGoyTh3/NqE3eXAT1njTvelh0338k=
|
||||||
|
github.com/celestix/telegraph-go/v2 v2.0.4/go.mod h1:vu2LtqM7MgOAJ2LDF8XK27DWdd1QYLBfZGhalEh086Y=
|
||||||
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.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/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/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||||
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
|
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
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=
|
||||||
@@ -23,8 +27,14 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
|
|||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
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/duke-git/lancet/v2 v2.3.5 h1:vb49UWkkdyu2eewilZbl0L3X3T133znSQG0FaeJIBMg=
|
||||||
|
github.com/duke-git/lancet/v2 v2.3.5/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=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw=
|
||||||
|
github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M=
|
||||||
|
github.com/eko/gocache/store/go_cache/v4 v4.2.2 h1:tAI9nl6TLoJyKG1ujF0CS0n/IgTEMl+NivxtR5R3/hw=
|
||||||
|
github.com/eko/gocache/store/go_cache/v4 v4.2.2/go.mod h1:T9zkHokzr8K9EiC7RfMbDg6HSwaV6rv3UdcNu13SGcA=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
@@ -32,8 +42,12 @@ 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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.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 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
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=
|
||||||
@@ -49,17 +63,20 @@ github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
|
|||||||
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
||||||
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
|
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
|
||||||
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
||||||
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
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-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
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=
|
||||||
@@ -72,6 +89,7 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
|||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf h1:BvBLUD2hkvLI3dJTJMiopAq8/wp43AAZKTP7qdpptbU=
|
github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf h1:BvBLUD2hkvLI3dJTJMiopAq8/wp43AAZKTP7qdpptbU=
|
||||||
github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||||
@@ -80,8 +98,12 @@ github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw=
|
|||||||
github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA=
|
github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA=
|
||||||
github.com/gookit/gsr v0.1.0 h1:0gadWaYGU4phMs0bma38t+Do5OZowRMEVlHv31p0Zig=
|
github.com/gookit/gsr v0.1.0 h1:0gadWaYGU4phMs0bma38t+Do5OZowRMEVlHv31p0Zig=
|
||||||
github.com/gookit/gsr v0.1.0/go.mod h1:7wv4Y4WCnil8+DlDYHBjidzrEzfHhXEoFjEA0pPPWpI=
|
github.com/gookit/gsr v0.1.0/go.mod h1:7wv4Y4WCnil8+DlDYHBjidzrEzfHhXEoFjEA0pPPWpI=
|
||||||
|
github.com/gookit/gsr v0.1.1 h1:TaHD3M7qa6lcAf9D2J4mGNg+QjgDtD1bw7uctF8RXOM=
|
||||||
|
github.com/gookit/gsr v0.1.1/go.mod h1:7wv4Y4WCnil8+DlDYHBjidzrEzfHhXEoFjEA0pPPWpI=
|
||||||
github.com/gookit/slog v0.5.7 h1:n3Dhgmr3NP+KppkNg95+vpFcI4YD1csu9VTQwgcEYTs=
|
github.com/gookit/slog v0.5.7 h1:n3Dhgmr3NP+KppkNg95+vpFcI4YD1csu9VTQwgcEYTs=
|
||||||
github.com/gookit/slog v0.5.7/go.mod h1:uWCRB4YO+FmgwXEq3s8U7ob1wWP7RStOuY/2a4yC/8o=
|
github.com/gookit/slog v0.5.7/go.mod h1:uWCRB4YO+FmgwXEq3s8U7ob1wWP7RStOuY/2a4yC/8o=
|
||||||
|
github.com/gookit/slog v0.5.8 h1:XZCeHLQvvOZWcSUDZcqxXITsL9+d1ESsKZoASBmK1lI=
|
||||||
|
github.com/gookit/slog v0.5.8/go.mod h1:s0ViFOY/IgUuT4MDPF0l9x5/npcciy8pL4xwWZadnoc=
|
||||||
github.com/gotd/contrib v0.21.0 h1:4Fj05jnyBE84toXZl7mVTvt7f732n5uglvztyG6nTr4=
|
github.com/gotd/contrib v0.21.0 h1:4Fj05jnyBE84toXZl7mVTvt7f732n5uglvztyG6nTr4=
|
||||||
github.com/gotd/contrib v0.21.0/go.mod h1:ENoUh75IhHGxfz/puVJg8BU4ZF89yrL6Q47TyoNqFYo=
|
github.com/gotd/contrib v0.21.0/go.mod h1:ENoUh75IhHGxfz/puVJg8BU4ZF89yrL6Q47TyoNqFYo=
|
||||||
github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
|
github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
|
||||||
@@ -90,6 +112,8 @@ github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
|||||||
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
||||||
github.com/gotd/td v0.120.0 h1:XeiafJM82/9SaB+ZMjMm/dnUx5+avINwVZOEsnV0zMo=
|
github.com/gotd/td v0.120.0 h1:XeiafJM82/9SaB+ZMjMm/dnUx5+avINwVZOEsnV0zMo=
|
||||||
github.com/gotd/td v0.120.0/go.mod h1:BCc2jFj1l5zP9Trk4J7nxeqW0KBGl6K95eXMgszkbOI=
|
github.com/gotd/td v0.120.0/go.mod h1:BCc2jFj1l5zP9Trk4J7nxeqW0KBGl6K95eXMgszkbOI=
|
||||||
|
github.com/gotd/td v0.123.0 h1:n6QKwGuguP7wZJsGBSGuFHziMmrp0koB6ecYqGyjrSc=
|
||||||
|
github.com/gotd/td v0.123.0/go.mod h1:iNYgJdwdIg9yaDfM18jrZuVVsbmQoMH6HyEY0GMBhXw=
|
||||||
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=
|
||||||
@@ -97,20 +121,19 @@ github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7V
|
|||||||
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=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
|
||||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -118,41 +141,77 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.81 h1:SzhMN0TQ6T/xSBu6Nvw3M5M8voM+Ht8RH3hE8S7zxaA=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.81/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5JQc=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
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.10.0 h1:x3ukRtq/pdn/k8+pYBtqWceVASiSmgK9M5lrH89Q+04=
|
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/ogen-go/ogen v1.10.0/go.mod h1:WExXrswerPzGWD0NpzBFsz+5eQIbP7HAtZUmpV8dqqI=
|
||||||
|
github.com/ogen-go/ogen v1.12.0 h1:JMkn957i9/IPaSehqpblviy6Uao3eqQ+eVKUn4LM9pg=
|
||||||
|
github.com/ogen-go/ogen v1.12.0/go.mod h1:RL25amedfhq5xKTUuPBPn6nhYU59CWaVWYJ8YIjNHs0=
|
||||||
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/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=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
|
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||||
|
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||||
|
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||||
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||||
|
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||||
|
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||||
|
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||||
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
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=
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg=
|
github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
@@ -161,19 +220,25 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS
|
|||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||||
|
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||||
|
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
||||||
|
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||||
|
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
|
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/studio-b12/gowebdav v0.10.0 h1:Yewz8FFiadcGEu4hxS/AAJQlHelndqln1bns3hcJIYc=
|
|
||||||
github.com/studio-b12/gowebdav v0.10.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||||
@@ -185,69 +250,124 @@ 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/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
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=
|
||||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||||
|
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||||
|
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||||
|
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||||
|
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
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.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||||
|
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||||
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
|
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||||
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=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
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.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
|
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/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
|
||||||
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||||
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
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.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
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-20190423024810-112230192c58/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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
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.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||||
|
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||||
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
|
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||||
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
|
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
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/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||||
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
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=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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=
|
||||||
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
@@ -257,39 +377,40 @@ 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/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
|
|
||||||
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
|
|
||||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
|
||||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
|
||||||
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
|
|
||||||
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
|
|
||||||
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=
|
||||||
|
gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs=
|
||||||
|
gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
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/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
|
||||||
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
||||||
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
|
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
|
||||||
|
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
|
||||||
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/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
||||||
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
||||||
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||||
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||||
|
modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y=
|
||||||
|
modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs=
|
||||||
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=
|
||||||
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||||
|
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
||||||
|
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
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.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw=
|
modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw=
|
||||||
modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic=
|
modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic=
|
||||||
|
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||||
|
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||||
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=
|
||||||
|
|||||||
@@ -8,30 +8,65 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TaskQueue struct {
|
type TaskQueue struct {
|
||||||
list *list.List
|
list *list.List
|
||||||
cond *sync.Cond
|
cond *sync.Cond
|
||||||
mutex *sync.Mutex
|
mutex *sync.Mutex
|
||||||
|
activeMap map[string]*types.Task
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *TaskQueue) AddTask(task types.Task) {
|
func (q *TaskQueue) AddTask(task *types.Task) {
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
defer q.mutex.Unlock()
|
defer q.mutex.Unlock()
|
||||||
q.list.PushBack(task)
|
q.list.PushBack(task)
|
||||||
q.cond.Signal()
|
q.cond.Signal()
|
||||||
|
if task.Status != types.Pending {
|
||||||
|
delete(q.activeMap, task.Key())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *TaskQueue) GetTask() types.Task {
|
func (q *TaskQueue) GetTask() *types.Task {
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
defer q.mutex.Unlock()
|
defer q.mutex.Unlock()
|
||||||
for q.list.Len() == 0 {
|
for q.list.Len() == 0 {
|
||||||
q.cond.Wait()
|
q.cond.Wait()
|
||||||
}
|
}
|
||||||
e := q.list.Front()
|
e := q.list.Front()
|
||||||
task := e.Value.(types.Task)
|
task := e.Value.(*types.Task)
|
||||||
q.list.Remove(e)
|
q.list.Remove(e)
|
||||||
|
if task.Status == types.Pending {
|
||||||
|
q.activeMap[task.Key()] = task
|
||||||
|
}
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *TaskQueue) DoneTask(task *types.Task) {
|
||||||
|
q.mutex.Lock()
|
||||||
|
defer q.mutex.Unlock()
|
||||||
|
delete(q.activeMap, task.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *TaskQueue) CancelTask(key string) bool {
|
||||||
|
q.mutex.Lock()
|
||||||
|
defer q.mutex.Unlock()
|
||||||
|
if task, ok := q.activeMap[key]; ok {
|
||||||
|
if task.Cancel != nil {
|
||||||
|
task.Cancel()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for e := q.list.Front(); e != nil; e = e.Next() {
|
||||||
|
task := e.Value.(*types.Task)
|
||||||
|
if task.Key() == key {
|
||||||
|
if task.Cancel != nil {
|
||||||
|
task.Cancel()
|
||||||
|
}
|
||||||
|
q.list.Remove(e)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (q *TaskQueue) Len() int {
|
func (q *TaskQueue) Len() int {
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
defer q.mutex.Unlock()
|
defer q.mutex.Unlock()
|
||||||
@@ -47,20 +82,29 @@ func init() {
|
|||||||
func NewQueue() *TaskQueue {
|
func NewQueue() *TaskQueue {
|
||||||
m := &sync.Mutex{}
|
m := &sync.Mutex{}
|
||||||
return &TaskQueue{
|
return &TaskQueue{
|
||||||
list: list.New(),
|
list: list.New(),
|
||||||
cond: sync.NewCond(m),
|
cond: sync.NewCond(m),
|
||||||
mutex: m,
|
mutex: m,
|
||||||
|
activeMap: make(map[string]*types.Task),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddTask(task types.Task) {
|
func AddTask(task *types.Task) {
|
||||||
Queue.AddTask(task)
|
Queue.AddTask(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTask() types.Task {
|
func GetTask() *types.Task {
|
||||||
return Queue.GetTask()
|
return Queue.GetTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Len() int {
|
func Len() int {
|
||||||
return Queue.Len()
|
return Queue.Len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CancelTask(key string) bool {
|
||||||
|
return Queue.CancelTask(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DoneTask(task *types.Task) {
|
||||||
|
Queue.DoneTask(task)
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
config "github.com/krau/SaveAny-Bot/config/storage"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,36 +41,36 @@ func (a *Alist) Init(cfg config.StorageConfig) error {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"/api/me", nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"/api/me", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Fatalf("Failed to create request: %v", err)
|
common.Log.Fatalf("Failed to create request: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", a.token)
|
req.Header.Set("Authorization", a.token)
|
||||||
|
|
||||||
resp, err := a.client.Do(req)
|
resp, err := a.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Fatalf("Failed to send request: %v", err)
|
common.Log.Fatalf("Failed to send request: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
logger.L.Fatalf("Failed to get alist user info: %s", resp.Status)
|
common.Log.Fatalf("Failed to get alist user info: %s", resp.Status)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Fatalf("Failed to read response body: %v", err)
|
common.Log.Fatalf("Failed to read response body: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var meResp meResponse
|
var meResp meResponse
|
||||||
if err := json.Unmarshal(body, &meResp); err != nil {
|
if err := json.Unmarshal(body, &meResp); err != nil {
|
||||||
logger.L.Fatalf("Failed to unmarshal me response: %v", err)
|
common.Log.Fatalf("Failed to unmarshal me response: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if meResp.Code != http.StatusOK {
|
if meResp.Code != http.StatusOK {
|
||||||
logger.L.Fatalf("Failed to get alist user info: %s", meResp.Message)
|
common.Log.Fatalf("Failed to get alist user info: %s", meResp.Message)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.L.Debugf("Logged in Alist as %s", meResp.Data.Username)
|
common.Log.Debugf("Logged in Alist as %s", meResp.Data.Username)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
a.loginInfo = &loginRequest{
|
a.loginInfo = &loginRequest{
|
||||||
@@ -80,10 +79,10 @@ func (a *Alist) Init(cfg config.StorageConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := a.getToken(); err != nil {
|
if err := a.getToken(); err != nil {
|
||||||
logger.L.Fatalf("Failed to login to Alist: %v", err)
|
common.Log.Fatalf("Failed to login to Alist: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.L.Debug("Logged in to Alist")
|
common.Log.Debug("Logged in to Alist")
|
||||||
|
|
||||||
go a.refreshToken(*alistConfig)
|
go a.refreshToken(*alistConfig)
|
||||||
return nil
|
return nil
|
||||||
@@ -97,27 +96,22 @@ func (a *Alist) Name() string {
|
|||||||
return a.config.Name
|
return a.config.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
|
func (a *Alist) Save(ctx context.Context, reader io.Reader, storagePath string) error {
|
||||||
file, err := os.Open(filePath)
|
common.Log.Infof("Saving file to %s", storagePath)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open file: %w", err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
filestat, err := file.Stat()
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, a.baseURL+"/api/fs/put", reader)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get file stats: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, a.baseURL+"/api/fs/put", file)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create request: %w", err)
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", a.token)
|
req.Header.Set("Authorization", a.token)
|
||||||
req.Header.Set("File-Path", url.PathEscape(storagePath))
|
req.Header.Set("File-Path", url.PathEscape(storagePath))
|
||||||
req.Header.Set("As-Task", "true")
|
|
||||||
req.Header.Set("Content-Type", "application/octet-stream")
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
req.ContentLength = filestat.Size()
|
if length := ctx.Value(types.ContextKeyContentLength); length != nil {
|
||||||
|
length, ok := length.(int64)
|
||||||
|
if ok {
|
||||||
|
req.ContentLength = length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := a.client.Do(req)
|
resp, err := a.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -146,6 +140,10 @@ func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Alist) NotSupportStream() string {
|
||||||
|
return "Alist does not support chunked transfer encoding"
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Alist) JoinStoragePath(task types.Task) string {
|
func (a *Alist) JoinStoragePath(task types.Task) string {
|
||||||
return path.Join(a.config.BasePath, task.StoragePath)
|
return path.Join(a.config.BasePath, task.StoragePath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
config "github.com/krau/SaveAny-Bot/config/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *Alist) getToken() error {
|
func (a *Alist) getToken() error {
|
||||||
@@ -51,15 +51,15 @@ func (a *Alist) getToken() error {
|
|||||||
func (a *Alist) refreshToken(cfg config.AlistStorageConfig) {
|
func (a *Alist) refreshToken(cfg config.AlistStorageConfig) {
|
||||||
tokenExp := cfg.TokenExp
|
tokenExp := cfg.TokenExp
|
||||||
if tokenExp <= 0 {
|
if tokenExp <= 0 {
|
||||||
logger.L.Warn("Invalid token expiration time, using default value")
|
common.Log.Warn("Invalid token expiration time, using default value")
|
||||||
tokenExp = 3600
|
tokenExp = 3600
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
time.Sleep(time.Duration(tokenExp) * time.Second)
|
time.Sleep(time.Duration(tokenExp) * time.Second)
|
||||||
if err := a.getToken(); err != nil {
|
if err := a.getToken(); err != nil {
|
||||||
logger.L.Errorf("Failed to refresh jwt token: %v", err)
|
common.Log.Errorf("Failed to refresh jwt token: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
logger.L.Info("Refreshed Alist jwt token")
|
common.Log.Info("Refreshed Alist jwt token")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package local
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"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/common"
|
||||||
|
config "github.com/krau/SaveAny-Bot/config/storage"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,7 +41,13 @@ func (l *Local) Name() string {
|
|||||||
return l.config.Name
|
return l.config.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Local) Save(ctx context.Context, filePath, storagePath string) error {
|
func (l *Local) JoinStoragePath(task types.Task) string {
|
||||||
|
return filepath.Join(l.config.BasePath, task.StoragePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Local) Save(ctx context.Context, r io.Reader, storagePath string) error {
|
||||||
|
common.Log.Infof("Saving file to %s", storagePath)
|
||||||
|
|
||||||
absPath, err := filepath.Abs(storagePath)
|
absPath, err := filepath.Abs(storagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -47,9 +55,11 @@ func (l *Local) Save(ctx context.Context, filePath, storagePath string) error {
|
|||||||
if err := fileutil.CreateDir(filepath.Dir(absPath)); err != nil {
|
if err := fileutil.CreateDir(filepath.Dir(absPath)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return fileutil.CopyFile(filePath, storagePath)
|
file, err := os.Create(absPath)
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
func (l *Local) JoinStoragePath(task types.Task) string {
|
}
|
||||||
return filepath.Join(l.config.BasePath, task.StoragePath)
|
defer file.Close()
|
||||||
|
_, err = io.Copy(file, r)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
72
storage/minio/client.go
Normal file
72
storage/minio/client.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package minio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
config "github.com/krau/SaveAny-Bot/config/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Minio struct {
|
||||||
|
config config.MinioStorageConfig
|
||||||
|
client *minio.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Minio) Init(cfg config.StorageConfig) error {
|
||||||
|
minioConfig, ok := cfg.(*config.MinioStorageConfig)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("failed to cast minio config")
|
||||||
|
}
|
||||||
|
if err := minioConfig.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.config = *minioConfig
|
||||||
|
|
||||||
|
client, err := minio.New(m.config.Endpoint, &minio.Options{
|
||||||
|
Creds: credentials.NewStaticV4(m.config.AccessKeyID, m.config.SecretAccessKey, ""),
|
||||||
|
Secure: m.config.UseSSL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create minio client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := client.BucketExists(context.Background(), m.config.BucketName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check bucket existence: %w", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("bucket %s does not exist", m.config.BucketName)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.client = client
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Minio) Type() types.StorageType {
|
||||||
|
return types.StorageTypeMinio
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Minio) Name() string {
|
||||||
|
return m.config.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Minio) JoinStoragePath(task types.Task) string {
|
||||||
|
return path.Join(m.config.BasePath, task.StoragePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Minio) Save(ctx context.Context, r io.Reader, storagePath string) error {
|
||||||
|
common.Log.Infof("Saving file from reader to %s", storagePath)
|
||||||
|
|
||||||
|
_, err := m.client.PutObject(ctx, m.config.BucketName, storagePath, r, -1, minio.PutObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to upload file to minio: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,25 +3,35 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
sc "github.com/krau/SaveAny-Bot/config/storage"
|
||||||
"github.com/krau/SaveAny-Bot/storage/alist"
|
"github.com/krau/SaveAny-Bot/storage/alist"
|
||||||
"github.com/krau/SaveAny-Bot/storage/local"
|
"github.com/krau/SaveAny-Bot/storage/local"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage/minio"
|
||||||
"github.com/krau/SaveAny-Bot/storage/webdav"
|
"github.com/krau/SaveAny-Bot/storage/webdav"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
Init(cfg config.StorageConfig) error
|
Init(cfg sc.StorageConfig) error
|
||||||
Type() types.StorageType
|
Type() types.StorageType
|
||||||
Name() string
|
Name() string
|
||||||
JoinStoragePath(task types.Task) string
|
JoinStoragePath(task types.Task) string
|
||||||
Save(cttx context.Context, localFilePath, storagePath string) error
|
Save(ctx context.Context, reader io.Reader, storagePath string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageNotSupportStream interface {
|
||||||
|
Storage
|
||||||
|
NotSupportStream() string
|
||||||
}
|
}
|
||||||
|
|
||||||
var Storages = make(map[string]Storage)
|
var Storages = make(map[string]Storage)
|
||||||
|
|
||||||
|
var UserStorages = make(map[int64][]Storage)
|
||||||
|
|
||||||
// GetStorageByName returns storage by name from cache or creates new one
|
// GetStorageByName returns storage by name from cache or creates new one
|
||||||
func GetStorageByName(name string) (Storage, error) {
|
func GetStorageByName(name string) (Storage, error) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
@@ -59,6 +69,12 @@ func GetStorageByUserIDAndName(chatID int64, name string) (Storage, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetUserStorages(chatID int64) []Storage {
|
func GetUserStorages(chatID int64) []Storage {
|
||||||
|
if chatID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if storages, ok := UserStorages[chatID]; ok {
|
||||||
|
return storages
|
||||||
|
}
|
||||||
var storages []Storage
|
var storages []Storage
|
||||||
for _, name := range config.Cfg.GetStorageNamesByUserID(chatID) {
|
for _, name := range config.Cfg.GetStorageNamesByUserID(chatID) {
|
||||||
storage, err := GetStorageByName(name)
|
storage, err := GetStorageByName(name)
|
||||||
@@ -76,9 +92,10 @@ var storageConstructors = map[string]StorageConstructor{
|
|||||||
string(types.StorageTypeAlist): func() Storage { return new(alist.Alist) },
|
string(types.StorageTypeAlist): func() Storage { return new(alist.Alist) },
|
||||||
string(types.StorageTypeLocal): func() Storage { return new(local.Local) },
|
string(types.StorageTypeLocal): func() Storage { return new(local.Local) },
|
||||||
string(types.StorageTypeWebdav): func() Storage { return new(webdav.Webdav) },
|
string(types.StorageTypeWebdav): func() Storage { return new(webdav.Webdav) },
|
||||||
|
string(types.StorageTypeMinio): func() Storage { return new(minio.Minio) },
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStorage(cfg config.StorageConfig) (Storage, error) {
|
func NewStorage(cfg sc.StorageConfig) (Storage, error) {
|
||||||
constructor, ok := storageConstructors[string(cfg.GetType())]
|
constructor, ok := storageConstructors[string(cfg.GetType())]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("不支持的存储类型: %s", cfg.GetType())
|
return nil, fmt.Errorf("不支持的存储类型: %s", cfg.GetType())
|
||||||
@@ -93,12 +110,15 @@ func NewStorage(cfg config.StorageConfig) (Storage, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LoadStorages() {
|
func LoadStorages() {
|
||||||
logger.L.Info("加载存储...")
|
common.Log.Info("加载存储...")
|
||||||
for _, storage := range config.Cfg.Storages {
|
for _, storage := range config.Cfg.Storages {
|
||||||
_, err := GetStorageByName(storage.GetName())
|
_, err := GetStorageByName(storage.GetName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Errorf("加载存储 %s 失败: %v", storage.GetName(), err)
|
common.Log.Errorf("加载存储 %s 失败: %v", storage.GetName(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.L.Infof("成功加载 %d 个存储", len(Storages))
|
common.Log.Infof("成功加载 %d 个存储", len(Storages))
|
||||||
|
for user := range config.Cfg.GetUsersID() {
|
||||||
|
UserStorages[int64(user)] = GetUserStorages(int64(user))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
135
storage/webdav/client.go
Normal file
135
storage/webdav/client.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package webdav
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
BaseURL string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebdavMethod string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WebdavMethodMkcol WebdavMethod = "MKCOL"
|
||||||
|
WebdavMethodPropfind WebdavMethod = "PROPFIND"
|
||||||
|
WebdavMethodPut WebdavMethod = "PUT"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewClient(baseURL, username, password string, httpClient *http.Client) *Client {
|
||||||
|
if !strings.HasSuffix(baseURL, "/") {
|
||||||
|
baseURL += "/"
|
||||||
|
}
|
||||||
|
if httpClient == nil {
|
||||||
|
httpClient = http.DefaultClient
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
BaseURL: baseURL,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
httpClient: httpClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doRequest(ctx context.Context, method WebdavMethod, url string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, string(method), url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c.Username != "" && c.Password != "" {
|
||||||
|
req.SetBasicAuth(c.Username, c.Password)
|
||||||
|
}
|
||||||
|
if method == WebdavMethodPropfind {
|
||||||
|
req.Header.Set("Depth", "1")
|
||||||
|
}
|
||||||
|
if method == WebdavMethodPut && ctx != nil {
|
||||||
|
if length := ctx.Value(types.ContextKeyContentLength); length != nil {
|
||||||
|
if l, ok := length.(int64); ok {
|
||||||
|
req.ContentLength = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.httpClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Exists(ctx context.Context, remotePath string) (bool, error) {
|
||||||
|
url := c.BaseURL + remotePath
|
||||||
|
resp, err := c.doRequest(ctx, WebdavMethodPropfind, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("PROPFIND: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) MkDir(ctx context.Context, dirPath string) error {
|
||||||
|
dirPath = strings.Trim(dirPath, "/")
|
||||||
|
if dirPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts := strings.Split(dirPath, "/")
|
||||||
|
currentPath := ""
|
||||||
|
for i, part := range parts {
|
||||||
|
if i > 0 {
|
||||||
|
currentPath += "/"
|
||||||
|
}
|
||||||
|
currentPath += part
|
||||||
|
|
||||||
|
exists, err := c.Exists(ctx, currentPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
url := c.BaseURL + currentPath
|
||||||
|
resp, err := c.doRequest(ctx, WebdavMethodMkcol, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("MKCOL %s: %s", currentPath, resp.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WriteFile(ctx context.Context, remotePath string, content io.Reader) error {
|
||||||
|
u, err := url.Parse(c.BaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parts := strings.Split(strings.Trim(remotePath, "/"), "/")
|
||||||
|
u.Path = path.Join(u.Path, strings.Join(parts, "/"))
|
||||||
|
resp, err := c.doRequest(ctx, WebdavMethodPut, u.String(), content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("PUT: %s", resp.Status)
|
||||||
|
|
||||||
|
}
|
||||||
130
storage/webdav/client_test.go
Normal file
130
storage/webdav/client_test.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package webdav
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/net/webdav"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupWebDAVServer(t *testing.T) (*httptest.Server, string) {
|
||||||
|
t.Helper()
|
||||||
|
tempDir, err := os.MkdirTemp("", "webdav_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mk temp dir failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := &webdav.Handler{
|
||||||
|
Prefix: "/",
|
||||||
|
FileSystem: webdav.Dir(tempDir),
|
||||||
|
LockSystem: webdav.NewMemLS(),
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
return server, tempDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMkDirAndExists(t *testing.T) {
|
||||||
|
server, tempDir := setupWebDAVServer(t)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL, "", "", nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
testpaths := []string{"testdir", "testdir/subdir", "testdir/子目录", "/testdir/测试路径/测试路径2"}
|
||||||
|
for _, p := range testpaths {
|
||||||
|
exists, err := client.Exists(ctx, p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Call Exists Err: %v", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
t.Fatalf("Dir should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.MkDir(ctx, p); err != nil {
|
||||||
|
t.Fatalf("Call MkDir Err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err = client.Exists(ctx, p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Call Exists Err: %v", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
t.Fatalf("Dir should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteFile(t *testing.T) {
|
||||||
|
server, tempDir := setupWebDAVServer(t)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL, "", "", nil)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
remotePath string
|
||||||
|
content string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
remotePath: "hello.txt",
|
||||||
|
content: "Hello webdav",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
remotePath: "nested/dir/test.txt",
|
||||||
|
content: "Nested file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
remotePath: "empty.txt",
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
remotePath: "unicode.txt",
|
||||||
|
content: "测试",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.remotePath, func(t *testing.T) {
|
||||||
|
dir := path.Dir(tc.remotePath)
|
||||||
|
if dir != "." {
|
||||||
|
if err := client.MkDir(ctx, dir); err != nil {
|
||||||
|
t.Fatalf("创建目录 %s 失败: %v", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.WriteFile(ctx, tc.remotePath, strings.NewReader(tc.content)); err != nil {
|
||||||
|
t.Fatalf("写入文件 %s 失败: %v", tc.remotePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
localPath := filepath.Join(tempDir, tc.remotePath)
|
||||||
|
data, err := os.ReadFile(localPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("读取文件 %s 失败: %v", localPath, err)
|
||||||
|
}
|
||||||
|
if string(data) != tc.content {
|
||||||
|
t.Fatalf("文件内容不匹配: got %s, want %s", string(data), tc.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
appended := tc.content + " Overwritten."
|
||||||
|
if err := client.WriteFile(ctx, tc.remotePath, strings.NewReader(appended)); err != nil {
|
||||||
|
t.Fatalf("覆盖写入文件 %s 失败: %v", tc.remotePath, err)
|
||||||
|
}
|
||||||
|
data, err = os.ReadFile(localPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("读取覆盖后的文件 %s 失败: %v", localPath, err)
|
||||||
|
}
|
||||||
|
if string(data) != appended {
|
||||||
|
t.Fatalf("文件覆盖后的内容不匹配: got %s, want %s", string(data), appended)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,19 +3,19 @@ package webdav
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"io"
|
||||||
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
config "github.com/krau/SaveAny-Bot/config/storage"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
"github.com/studio-b12/gowebdav"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Webdav struct {
|
type Webdav struct {
|
||||||
config config.WebdavStorageConfig
|
config config.WebdavStorageConfig
|
||||||
client *gowebdav.Client
|
client *Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Webdav) Init(cfg config.StorageConfig) error {
|
func (w *Webdav) Init(cfg config.StorageConfig) error {
|
||||||
@@ -27,12 +27,9 @@ func (w *Webdav) Init(cfg config.StorageConfig) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.config = *webdavConfig
|
w.config = *webdavConfig
|
||||||
client := gowebdav.NewClient(webdavConfig.URL, webdavConfig.Username, webdavConfig.Password)
|
w.client = NewClient(w.config.URL, w.config.Username, w.config.Password, &http.Client{
|
||||||
if err := client.Connect(); err != nil {
|
Timeout: time.Hour * 12,
|
||||||
return fmt.Errorf("failed to connect to webdav server: %w", err)
|
})
|
||||||
}
|
|
||||||
client.SetTimeout(12 * time.Hour)
|
|
||||||
w.client = client
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,25 +41,19 @@ func (w *Webdav) Name() string {
|
|||||||
return w.config.Name
|
return w.config.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error {
|
func (w *Webdav) JoinStoragePath(task types.Task) string {
|
||||||
if err := w.client.MkdirAll(path.Dir(storagePath), os.ModePerm); err != nil {
|
return path.Join(w.config.BasePath, task.StoragePath)
|
||||||
logger.L.Errorf("Failed to create directory %s: %v", path.Dir(storagePath), err)
|
}
|
||||||
|
|
||||||
|
func (w *Webdav) Save(ctx context.Context, r io.Reader, storagePath string) error {
|
||||||
|
common.Log.Infof("Saving file to %s", storagePath)
|
||||||
|
if err := w.client.MkDir(ctx, path.Dir(storagePath)); err != nil {
|
||||||
|
common.Log.Errorf("Failed to create directory %s: %v", path.Dir(storagePath), err)
|
||||||
return ErrFailedToCreateDirectory
|
return ErrFailedToCreateDirectory
|
||||||
}
|
}
|
||||||
file, err := os.Open(filePath)
|
if err := w.client.WriteFile(ctx, storagePath, r); err != nil {
|
||||||
if err != nil {
|
common.Log.Errorf("Failed to write file %s: %v", storagePath, err)
|
||||||
logger.L.Errorf("Failed to open file %s: %v", filePath, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
if err := w.client.WriteStream(storagePath, file, os.ModePerm); err != nil {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
82
types/task.go
Normal file
82
types/task.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Cancel context.CancelFunc
|
||||||
|
Error error
|
||||||
|
Status TaskStatus
|
||||||
|
StorageName string
|
||||||
|
StoragePath string
|
||||||
|
StartTime time.Time
|
||||||
|
|
||||||
|
File *File
|
||||||
|
FileMessageID int
|
||||||
|
FileChatID int64
|
||||||
|
|
||||||
|
IsTelegraph bool
|
||||||
|
TelegraphURL string
|
||||||
|
|
||||||
|
// to track the reply message
|
||||||
|
ReplyMessageID int
|
||||||
|
ReplyChatID int64
|
||||||
|
UserID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Task) Key() string {
|
||||||
|
if t.IsTelegraph {
|
||||||
|
return hashStr(t.TelegraphURL)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d:%d", t.FileChatID, t.FileMessageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Task) String() string {
|
||||||
|
if t.IsTelegraph {
|
||||||
|
return fmt.Sprintf("[telegraph]:%s", t.TelegraphURL)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%d:%d]:%s", t.FileChatID, t.FileMessageID, t.File.FileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Task) FileName() string {
|
||||||
|
if t.IsTelegraph {
|
||||||
|
tgphPath := strings.Split(t.TelegraphURL, "/")[len(strings.Split(t.TelegraphURL, "/"))-1]
|
||||||
|
tgphPathUnescaped, err := url.PathUnescape(tgphPath)
|
||||||
|
if err != nil {
|
||||||
|
return tgphPath
|
||||||
|
}
|
||||||
|
return tgphPathUnescaped
|
||||||
|
}
|
||||||
|
return t.File.FileName
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Location tg.InputFileLocationClass
|
||||||
|
FileSize int64
|
||||||
|
FileName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) Hash() string {
|
||||||
|
locationBytes := []byte(f.Location.String())
|
||||||
|
fileSizeBytes := []byte(fmt.Sprintf("%d", f.FileSize))
|
||||||
|
fileNameBytes := []byte(f.FileName)
|
||||||
|
|
||||||
|
structBytes := append(locationBytes, fileSizeBytes...)
|
||||||
|
structBytes = append(structBytes, fileNameBytes...)
|
||||||
|
|
||||||
|
hash := md5.New()
|
||||||
|
hash.Write(structBytes)
|
||||||
|
hashBytes := hash.Sum(nil)
|
||||||
|
|
||||||
|
return hex.EncodeToString(hashBytes)
|
||||||
|
}
|
||||||
@@ -1,18 +1,8 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gotd/td/tg"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TaskStatus string
|
type TaskStatus string
|
||||||
|
|
||||||
var (
|
const (
|
||||||
Pending TaskStatus = "pending"
|
Pending TaskStatus = "pending"
|
||||||
Succeeded TaskStatus = "succeeded"
|
Succeeded TaskStatus = "succeeded"
|
||||||
Failed TaskStatus = "failed"
|
Failed TaskStatus = "failed"
|
||||||
@@ -21,62 +11,32 @@ var (
|
|||||||
|
|
||||||
type StorageType string
|
type StorageType string
|
||||||
|
|
||||||
var (
|
const (
|
||||||
StorageTypeLocal StorageType = "local"
|
StorageTypeLocal StorageType = "local"
|
||||||
StorageTypeWebdav StorageType = "webdav"
|
StorageTypeWebdav StorageType = "webdav"
|
||||||
StorageTypeAlist StorageType = "alist"
|
StorageTypeAlist StorageType = "alist"
|
||||||
|
StorageTypeMinio StorageType = "minio"
|
||||||
)
|
)
|
||||||
|
|
||||||
var StorageTypes = []StorageType{StorageTypeLocal, StorageTypeAlist, StorageTypeWebdav}
|
var StorageTypes = []StorageType{StorageTypeLocal, StorageTypeAlist, StorageTypeWebdav, StorageTypeMinio}
|
||||||
var StorageTypeDisplay = map[StorageType]string{
|
var StorageTypeDisplay = map[StorageType]string{
|
||||||
StorageTypeLocal: "本地磁盘",
|
StorageTypeLocal: "本地磁盘",
|
||||||
StorageTypeWebdav: "WebDAV",
|
StorageTypeWebdav: "WebDAV",
|
||||||
StorageTypeAlist: "Alist",
|
StorageTypeAlist: "Alist",
|
||||||
|
StorageTypeMinio: "Minio",
|
||||||
}
|
}
|
||||||
|
|
||||||
type Task struct {
|
type ContextKey string
|
||||||
Ctx context.Context
|
|
||||||
Error error
|
|
||||||
Status TaskStatus
|
|
||||||
File *File
|
|
||||||
StorageName string
|
|
||||||
StoragePath string
|
|
||||||
StartTime time.Time
|
|
||||||
|
|
||||||
FileMessageID int
|
const (
|
||||||
FileChatID int64
|
ContextKeyContentLength ContextKey = "content-length"
|
||||||
// to track the reply message
|
)
|
||||||
ReplyMessageID int
|
|
||||||
ReplyChatID int64
|
|
||||||
// to track the user
|
|
||||||
UserID int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t Task) String() string {
|
type RuleType string
|
||||||
return fmt.Sprintf("[%d:%d]:%s", t.FileChatID, t.FileMessageID, t.File.FileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t Task) FileName() string {
|
const (
|
||||||
return t.File.FileName
|
RuleTypeFileNameRegex RuleType = "FILENAME-REGEX"
|
||||||
}
|
RuleTypeMessageRegex RuleType = "MESSAGE-REGEX"
|
||||||
|
)
|
||||||
|
|
||||||
type File struct {
|
var RuleTypes = []RuleType{RuleTypeFileNameRegex, RuleTypeMessageRegex}
|
||||||
Location tg.InputFileLocationClass
|
|
||||||
FileSize int64
|
|
||||||
FileName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f File) Hash() string {
|
|
||||||
locationBytes := []byte(f.Location.String())
|
|
||||||
fileSizeBytes := []byte(fmt.Sprintf("%d", f.FileSize))
|
|
||||||
fileNameBytes := []byte(f.FileName)
|
|
||||||
|
|
||||||
structBytes := append(locationBytes, fileSizeBytes...)
|
|
||||||
structBytes = append(structBytes, fileNameBytes...)
|
|
||||||
|
|
||||||
hash := md5.New()
|
|
||||||
hash.Write(structBytes)
|
|
||||||
hashBytes := hash.Sum(nil)
|
|
||||||
|
|
||||||
return hex.EncodeToString(hashBytes)
|
|
||||||
}
|
|
||||||
12
types/utils.go
Normal file
12
types/utils.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func hashStr(s string) string {
|
||||||
|
hash := md5.New()
|
||||||
|
hash.Write([]byte(s))
|
||||||
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user