Compare commits

...

76 Commits

Author SHA1 Message Date
krau
71844deab1 fix: public media copy message check 2025-05-28 17:02:51 +08:00
krau
55fed6389e chore: add experimental to nav 2025-05-28 16:56:38 +08:00
krau
8ce5c2e007 feat: 添加实验性功能文档,包含存储规则和媒体消息发送说明 2025-05-28 16:55:18 +08:00
krau
6ecfbd8385 feat: add public media copy 2025-05-28 16:43:11 +08:00
krau
6c2bfd72cd chore: commented-out code for user storages check, for we have send_here feature now 2025-05-28 16:10:31 +08:00
Krau
8ea5be5b90 Merge pull request #65 from krau/upload-telegram
feat: send media to telegram, close #47
2025-05-28 15:57:43 +08:00
krau
7f483056e0 feat: send media to telegram, close #47 2025-05-28 15:57:10 +08:00
krau
a6f88d7f75 chore: fix typo in Dockerfile ARG variable name for GitCommit 2025-05-19 22:14:50 +08:00
krau
b757df0b5e chore: reorganize Docker build workflow and enhance build arguments for versioning 2025-05-19 22:02:17 +08:00
krau
b017046c8b chore: simplify Dockerfile by removing unnecessary user and permission setup 2025-05-19 21:43:47 +08:00
krau
a474fdf6ae chore: update .dockerignore and Dockerfile for improved build context and permissions 2025-05-19 09:23:24 +08:00
krau
729e688748 fix: cleaning up the cache folder caused permission issues 2025-05-19 09:23:10 +08:00
krau
9ea4857cd9 chore: update issue template labels for consistency 2025-05-18 18:14:33 +08:00
krau
8bf7bc0e85 chore: add .dockerignore to exclude unnecessary files from Docker context 2025-05-18 18:14:30 +08:00
krau
26e344a6f6 refactor: remove unused conversation handling code and simplify delDir function parameters 2025-05-18 14:29:59 +08:00
krau
8f0744077e fix: update success message for batch task addition in handle_save function 2025-05-18 14:28:20 +08:00
krau
ed99a37831 fix: add unique id to task struct to avoid duplicate file name overwrite, close #59 2025-05-09 08:58:30 +08:00
krau
488d709d85 chore: update contributors section in README to remove specific name 2025-05-08 21:04:56 +08:00
krau
66454b082a fix: improve logger initialization and reduce cache TTL 2025-05-08 21:03:48 +08:00
Krau
70e83e62d9 Merge pull request #58 from AHCorn/main
fix: docker cache permission issue (#57)
2025-05-08 20:44:46 +08:00
安和
d2ddb9193a fix: docker cache permission issue (#57) 2025-05-08 18:38:28 +08:00
krau
5f78db90c7 fix: webdav url escape 2025-05-07 12:05:10 +08:00
krau
c3a4702e79 fix: allow custom file name for cached files in FileFromMessage function 2025-05-07 11:38:56 +08:00
krau
e731cfee9e chore: upgrade deps 2025-05-07 09:24:36 +08:00
krau
75de86fe97 chore: add funding configuration file 2025-05-07 09:21:30 +08:00
krau
6d4e97b4bb chore: add issue templates 2025-05-07 09:18:46 +08:00
krau
239d5ad562 feat: update database session configuration and retry settings 2025-05-07 08:59:15 +08:00
krau
e76f191922 chore: remove executable compression from build-release workflow 2025-04-29 09:37:25 +08:00
krau
a619ed2f22 typo: client_test filename 2025-04-27 08:32:47 +08:00
krau
838dfc35a1 fix: webdav client implement, close #49 2025-04-27 08:31:50 +08:00
krau
6ecee6d561 Merge branch 'main' of https://github.com/krau/SaveAny-Bot 2025-04-20 14:57:08 +08:00
krau
c1827f93a9 chore: rename workflow to 'Update Contributors' and trigger on workflow_dispatch 2025-04-20 14:57:06 +08:00
Krau
aaf3f7c35f Merge pull request #51 from krau/contributors-readme-action-yX7LIfZQ3S
docs(contributor): contributors readme action update
2025-04-20 14:56:49 +08:00
github-actions[bot]
02fbea4eb0 docs(contributor): contrib-readme-action has updated readme 2025-04-20 06:56:16 +00:00
krau
bf9aef6eb7 chore: update contributor workflow to trigger on workflow_dispatch instead of tags 2025-04-20 14:55:48 +08:00
krau
230c07fd55 feat: add rate limiting middleware to enhance bot performance 2025-04-20 14:50:06 +08:00
krau
18de349dc3 chore: update contributor workflow to trigger on tags and add contributor to README 2025-04-18 21:24:28 +08:00
krau
cef1a5c194 chore: update golang.org/x/net to latest 2025-04-18 21:19:41 +08:00
krau
99f8f0cb27 feat: add automated contributors section to README 2025-04-18 21:17:31 +08:00
krau
789c14134c chore: update .gitignore to include .vscode/ and remove launch.json 2025-04-18 21:12:59 +08:00
krau
5bb3b595aa docs: update contributing guides 2025-04-18 21:12:44 +08:00
krau
609289c16a perf: optimize user storage retrieval and remove unused rate limiting middleware 2025-04-15 21:04:48 +08:00
krau
c8c348a182 feat: batch save files 2025-04-12 16:27:23 +08:00
krau
725acd0199 feat: refactor caching logic to use gocache for better compatibility 2025-04-12 15:07:43 +08:00
krau
166c27c70f feat: automatic file organization based on rules, close #28 2025-04-12 14:27:13 +08:00
krau
3bdef20e85 feat: add expiration handling for database entries and enhance user model with rules 2025-04-12 11:14:13 +08:00
krau
50fba3f910 feat: add configurable timeout for Telegram client initialization 2025-04-07 10:23:50 +08:00
krau
87d3f14392 docs: update README to include sponsorship information and improve formatting 2025-04-05 00:01:48 +08:00
krau
30452c8d46 docs: consolidate message link information in help.md 2025-04-04 08:46:48 +08:00
krau
300f7723af fix: enhance webdav client impl 2025-03-31 17:34:24 +08:00
krau
491ba55f1e feat: add support for handling unsupported stream storage in download process 2025-03-26 10:35:40 +08:00
krau
32519b8c08 docs: add note about unsupported storage backends in Stream mode 2025-03-26 10:25:36 +08:00
krau
7ffd9891a0 fix: not pass content length when uploading in non stream mode 2025-03-26 10:22:38 +08:00
krau
347a60f1f7 fix: implement image extraction from Telegraph nodes 2025-03-24 22:04:55 +08:00
krau
da69fe1354 feat: enhance file name generation to include media extensions 2025-03-24 21:36:13 +08:00
krau
746ca026ba docs: remove outdated information about stream mode support in help documentation 2025-03-22 15:48:36 +08:00
krau
a8c64675e5 docs: update help documentation to include supported message links 2025-03-22 15:48:05 +08:00
krau
3918f6eee2 feat: add version and commit information to help text in start command 2025-03-22 15:45:34 +08:00
krau
8d44b43c82 fix: remove caching logic for Telegram messages in GetTGMessage function, close #40 2025-03-22 15:41:20 +08:00
krau
f14c4367f8 feat: cancel download telegraph task 2025-03-22 12:08:19 +08:00
krau
3e3a320672 feat: download telegraph images , close #5 2025-03-22 11:52:43 +08:00
krau
19efab0665 feat: implement GenFileNameFromMessage function for improved file naming 2025-03-22 09:33:50 +08:00
krau
635f00ac71 fix: reorganize cache destination path handling in processPendingTask function 2025-03-21 23:28:14 +08:00
krau
2d2becccf6 refactor: update storage interface to use io.Reader for Save method and remove stream implementations 2025-03-21 23:05:09 +08:00
krau
ed0837a89b refactor: replace logger usage with common.Log for consistent logging 2025-03-21 21:07:53 +08:00
krau
65fee89e14 feat: refactor storage configuration to use dedicated storage package and add new storage types
BREAKING CHANGE: remove deprecated config
2025-03-21 20:52:41 +08:00
krau
8e180006f0 chore: update dependencies to latest versions 2025-03-16 21:55:52 +08:00
krau
721c9666eb refactor: streamline storage configuration loading and remove redundant code 2025-03-11 22:24:52 +08:00
krau
6f35401181 docs: update links in README_EN.md for consistency 2025-03-11 21:46:04 +08:00
Krau
72ae2ce079 Merge pull request #35 from ysicing/main
feat: add Minio storage support
2025-03-11 21:41:43 +08:00
ysicing
495ad3ea5c feat: add Minio storage support
Signed-off-by: ysicing <i@ysicing.me>
2025-03-11 21:29:35 +08:00
krau
3def9df4b4 docs: update alist faq 2025-03-03 10:59:08 +08:00
krau
790a32d297 fix(alist): do not upload file as task to prevent alist cache full file 2025-03-03 10:58:03 +08:00
krau
f7779224ef docs: update example link 2025-03-01 15:54:21 +08:00
krau
7d899ae088 ci: Is anyone really using Windows ARM? 2025-03-01 14:01:10 +08:00
krau
7e67bdb7e2 fix: update executable compression condition for Windows ARM64 in build-release workflow 2025-03-01 13:55:42 +08:00
78 changed files with 2819 additions and 1443 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
*.md
.git
.github/
.gitignore
.vscode/
downloads/
data/
cache/
docs
config.example.toml
docker-compose.*

5
.github/FUNDING.yml vendored Normal file
View 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
View File

@@ -0,0 +1,35 @@
name: "👾 报告 bug"
description: "报告 bug"
labels:
- "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
View 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
View File

@@ -0,0 +1,33 @@
name: "⭐️ 功能请求"
description: "功能请求"
labels:
- "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:

View File

@@ -20,19 +20,6 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker - name: Extract metadata for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
@@ -50,11 +37,36 @@ jobs:
org.opencontainers.image.source=https://github.com/krau/SaveAny-Bot org.opencontainers.image.source=https://github.com/krau/SaveAny-Bot
org.opencontainers.image.url=https://github.com/krau/SaveAny-Bot org.opencontainers.image.url=https://github.com/krau/SaveAny-Bot
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version from Git Ref
id: extract_version
run: |
VERSION=$(echo "${{ github.ref }}" | sed 's/refs\/tags\/v//')
echo "VERSION=${VERSION}" >> $GITHUB_ENV
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ steps.meta.outputs.version }}
GitCommit=${{ github.sha }}
BuildTime=${{ format(github.event.repository.updated_at, 'yyyy-MM-dd HH:mm:ss') }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@@ -38,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
@@ -55,7 +58,6 @@ jobs:
goos: ${{ matrix.goos }} goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }} goarch: ${{ matrix.goarch }}
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
executable_compression: upx
extra_files: | extra_files: |
LICENSE LICENSE
README.md README.md

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

@@ -5,4 +5,5 @@ data/
downloads/ downloads/
cache/ cache/
session.* session.*
cache.db cache.db
.vscode/

15
.vscode/launch.json vendored
View File

@@ -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",
}
]
}

View File

@@ -1,13 +1,22 @@
FROM golang:alpine AS builder FROM golang:alpine AS builder
ARG VERSION="dev"
ARG GitCommit="Unknown"
ARG BuildTime="Unknown"
WORKDIR /app WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o saveany-bot . RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
CGO_ENABLED=0 \
go build -trimpath \
-ldflags "-s -w \
-X github.com/krau/SaveAny-Bot/common.Version=${VERSION} \
-X github.com/krau/SaveAny-Bot/common.GitCommit=${GiTCommit} \
-X github.com/krau/SaveAny-Bot/common.BuildTime=${BuildTime}" \
-o saveany-bot .
FROM alpine:latest FROM alpine:latest
@@ -15,4 +24,4 @@ WORKDIR /app
COPY --from=builder /app/saveany-bot . COPY --from=builder /app/saveany-bot .
CMD ["./saveany-bot"] ENTRYPOINT ["/app/saveany-bot"]

148
README.md
View File

@@ -2,7 +2,7 @@
# <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 的文件保存到各类存储端.
@@ -10,107 +10,59 @@
</div> </div>
## 部署 ## [部署](https://sabot.unv.app/deploy/)
### 从二进制文件部署 ## [参与开发](https://sabot.unv.app/contribute/)
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
在解压后目录新建 `config.toml` 文件, 参考 [config.example.toml](./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](./docs/saveanybot)自行修改.
`chmod +x /etc/init.d/saveanybot`
完成后,将文件复制到 `/etc/rc.d`并重命名为`S99saveanybot`.
`chmod +x /etc/rc.d/S99saveanybot`
#### 为OpenWrt及衍生系统添加快捷指令
创建文件` /usr/bin/sabot` ,参考[sabot](./docs/sabot)自行配置修改,注意此处文件编码仅支持 ANSI 936 .
`chmod +x /usr/bin/sabot`
之后,终端输入`sabot start|stop|restart|status|enable|disable`即可.
### 使用 Docker 部署
#### Docker Compose
下载 [docker-compose.yml](./docker-compose.yml) 文件, 在同目录下新建 `config.toml` 文件, 参考 [config.example.toml](./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)

View File

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

View File

@@ -3,7 +3,6 @@ package bot
import ( import (
"context" "context"
"net/url" "net/url"
"os"
"time" "time"
"github.com/celestix/gotgproto" "github.com/celestix/gotgproto"
@@ -11,8 +10,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 +26,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 +55,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 {
@@ -77,6 +78,7 @@ func Init() {
{Command: "storage", Description: "设置默认存储端"}, {Command: "storage", Description: "设置默认存储端"},
{Command: "save", Description: "保存所回复的文件"}, {Command: "save", Description: "保存所回复的文件"},
{Command: "dir", Description: "管理存储文件夹"}, {Command: "dir", Description: "管理存储文件夹"},
{Command: "rule", Description: "管理规则"},
}, },
}) })
resultChan <- struct { resultChan <- struct {
@@ -87,15 +89,13 @@ func Init() {
select { select {
case <-ctx.Done(): case <-ctx.Done():
logger.L.Fatal("初始化客户端失败: 超时") common.Log.Panic("初始化客户端失败: 超时")
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.Panicf("初始化客户端失败: %s", result.err)
os.Exit(1)
} }
Client = result.client Client = result.client
RegisterHandlers(Client.Dispatcher) RegisterHandlers(Client.Dispatcher)
logger.L.Info("客户端初始化完成") common.Log.Info("客户端初始化完成")
} }
} }

View File

@@ -13,16 +13,15 @@ import (
"github.com/gotd/td/telegram/message/entity" "github.com/gotd/td/telegram/message/entity"
"github.com/gotd/td/telegram/message/styling" "github.com/gotd/td/telegram/message/styling"
"github.com/gotd/td/tg" "github.com/gotd/td/tg"
"github.com/krau/SaveAny-Bot/common"
"github.com/krau/SaveAny-Bot/config" "github.com/krau/SaveAny-Bot/config"
"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/types" "github.com/krau/SaveAny-Bot/types"
"gorm.io/gorm" "gorm.io/gorm"
) )
func AddToQueue(ctx *ext.Context, update *ext.Update) error { func AddToQueue(ctx *ext.Context, update *ext.Update) error {
// TODO: 回调数据用户独立鉴权 (处理 bot 在群聊中的情况)
if !slice.Contain(config.Cfg.GetUsersID(), update.CallbackQuery.UserID) { if !slice.Contain(config.Cfg.GetUsersID(), update.CallbackQuery.UserID) {
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
@@ -33,11 +32,11 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
return dispatcher.EndGroups return dispatcher.EndGroups
} }
args := strings.Split(string(update.CallbackQuery.Data), " ") args := strings.Split(string(update.CallbackQuery.Data), " ")
addToDir := args[0] == "add_to_dir" addToDir := args[0] == "add_to_dir" // 已经选择了路径
cbDataId, _ := strconv.Atoi(args[1]) cbDataId, _ := strconv.Atoi(args[1])
cbData, err := dao.GetCallbackData(uint(cbDataId)) cbData, err := dao.GetCallbackData(uint(cbDataId))
if err != nil { if err != nil {
logger.L.Errorf("获取回调数据失败: %s", err) common.Log.Errorf("获取回调数据失败: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,
@@ -56,7 +55,7 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
user, err := dao.GetUserByChatID(update.CallbackQuery.UserID) user, err := dao.GetUserByChatID(update.CallbackQuery.UserID)
if err != nil { if err != nil {
logger.L.Errorf("获取用户失败: %s", err) common.Log.Errorf("获取用户失败: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,
@@ -69,7 +68,7 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
if !addToDir { if !addToDir {
dirs, err := dao.GetDirsByUserIDAndStorageName(user.ID, storageName) dirs, err := dao.GetDirsByUserIDAndStorageName(user.ID, storageName)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
logger.L.Errorf("获取路径失败: %s", err) common.Log.Errorf("获取路径失败: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,
@@ -81,7 +80,7 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
if len(dirs) != 0 { if len(dirs) != 0 {
markup, err := getSelectDirMarkup(fileChatID, fileMessageID, storageName, dirs) markup, err := getSelectDirMarkup(fileChatID, fileMessageID, storageName, dirs)
if err != nil { if err != nil {
logger.L.Errorf("获取路径失败: %s", err) common.Log.Errorf("获取路径失败: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,
@@ -96,16 +95,16 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
ReplyMarkup: markup, ReplyMarkup: markup,
}) })
if err != nil { if err != nil {
logger.L.Errorf("编辑消息失败: %s", err) common.Log.Errorf("编辑消息失败: %s", err)
} }
return dispatcher.EndGroups return dispatcher.EndGroups
} }
} }
logger.L.Tracef("Got add to queue: chatID: %d, messageID: %d, storage: %s", fileChatID, fileMessageID, storageName) common.Log.Tracef("Got add to queue: chatID: %d, messageID: %d, storage: %s", fileChatID, fileMessageID, storageName)
record, err := dao.GetReceivedFileByChatAndMessageID(int64(fileChatID), fileMessageID) record, err := dao.GetReceivedFileByChatAndMessageID(int64(fileChatID), fileMessageID)
if err != nil { if err != nil {
logger.L.Errorf("获取记录失败: %s", err) common.Log.Errorf("获取记录失败: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,
@@ -117,7 +116,7 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
if update.CallbackQuery.MsgID != record.ReplyMessageID { if update.CallbackQuery.MsgID != record.ReplyMessageID {
record.ReplyMessageID = update.CallbackQuery.MsgID record.ReplyMessageID = update.CallbackQuery.MsgID
if err := dao.SaveReceivedFile(record); err != nil { if err := dao.SaveReceivedFile(record); err != nil {
logger.L.Errorf("更新接收的文件失败: %s", err) common.Log.Errorf("更新记录失败: %s", err)
} }
} }
@@ -125,7 +124,7 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
if addToDir && dirId != 0 { if addToDir && dirId != 0 {
dir, err = dao.GetDirByID(dirId) dir, err = dao.GetDirByID(dirId)
if err != nil { if err != nil {
logger.L.Errorf("获取路径失败: %s", err) common.Log.Errorf("获取路径失败: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,
@@ -136,31 +135,51 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
} }
} }
file, err := FileFromMessage(ctx, record.ChatID, record.MessageID, record.FileName) var task types.Task
if err != nil { if record.IsTelegraph {
logger.L.Errorf("获取消息中的文件失败: %s", err) task = types.Task{
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ Ctx: ctx,
QueryID: update.CallbackQuery.QueryID, Status: types.Pending,
Alert: true, IsTelegraph: true,
Message: fmt.Sprintf("获取消息中的文件失败: %s", err), TelegraphURL: record.TelegraphURL,
CacheTime: 5, StorageName: storageName,
}) FileChatID: record.ChatID,
return dispatcher.EndGroups 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{ task = types.Task{
Ctx: ctx, Ctx: ctx,
Status: types.Pending, Status: types.Pending,
File: file, FileDBID: record.ID,
StorageName: storageName, File: file,
FileChatID: record.ChatID, StorageName: storageName,
ReplyMessageID: record.ReplyMessageID, FileChatID: record.ChatID,
FileMessageID: record.MessageID, ReplyMessageID: record.ReplyMessageID,
ReplyChatID: record.ReplyChatID, FileMessageID: record.MessageID,
UserID: update.GetUserChat().GetID(), ReplyChatID: record.ReplyChatID,
} UserID: update.GetUserChat().GetID(),
if dir != nil { }
task.StoragePath = path.Join(dir.Path, file.FileName) if dir != nil {
task.StoragePath = path.Join(dir.Path, file.FileName)
}
} }
queue.AddTask(&task) queue.AddTask(&task)
@@ -174,7 +193,7 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
styling.Plain("\n当前排队任务数: "), styling.Plain("\n当前排队任务数: "),
styling.Bold(strconv.Itoa(queue.Len())), styling.Bold(strconv.Itoa(queue.Len())),
); 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()
} }

View File

@@ -1,75 +0,0 @@
package bot
import (
"sync"
)
type ConversationType string
type ConversationState struct {
sync.Mutex
conversationType ConversationType
InConversation bool
data map[ConversationType]map[string]interface{}
}
func (c *ConversationState) Reset() {
c.Lock()
defer c.Unlock()
c.InConversation = false
c.conversationType = ""
c.data = make(map[ConversationType]map[string]interface{})
}
func (c *ConversationState) SetConversationType(t ConversationType) {
c.Lock()
defer c.Unlock()
c.conversationType = t
}
func (c *ConversationState) GetData(key string) interface{} {
if c.data == nil || c.data[c.conversationType] == nil {
return nil
}
return c.data[c.conversationType][key]
}
func (c *ConversationState) SetData(key string, value interface{}) {
c.Lock()
defer c.Unlock()
if c.data == nil {
c.data = make(map[ConversationType]map[string]interface{})
}
if c.data[c.conversationType] == nil {
c.data[c.conversationType] = make(map[string]interface{})
}
c.data[c.conversationType][key] = value
}
// TODO: Implement conversation handling
// var userConversationState = make(map[int64]*ConversationState)
// func handleConversation(ctx *ext.Context, update *ext.Update) error {
// userID := update.EffectiveUser().GetID()
// state, ok := userConversationState[userID]
// if !ok {
// return dispatcher.ContinueGroups
// }
// if update.EffectiveMessage.Text == "/cancel" {
// state.Reset()
// ctx.Reply(update, ext.ReplyTextString("已取消"), nil)
// return dispatcher.EndGroups
// }
// if !state.InConversation {
// return dispatcher.ContinueGroups
// }
// return handleConversationState(ctx, update, state)
// }
// func handleConversationState(ctx *ext.Context, update *ext.Update, state *ConversationState) error {
// switch state.conversationType {
// default:
// logger.L.Errorf("Unknown conversation type: %s", state.conversationType)
// }
// return dispatcher.EndGroups
// }

View File

@@ -1,61 +1,83 @@
package bot package bot
import ( import (
"fmt"
"strconv"
"strings" "strings"
"github.com/celestix/gotgproto/dispatcher" "github.com/celestix/gotgproto/dispatcher"
"github.com/celestix/gotgproto/ext" "github.com/celestix/gotgproto/ext"
"github.com/gotd/td/telegram/message/styling" "github.com/gotd/td/telegram/message/styling"
"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"
) )
func dirCmd(ctx *ext.Context, update *ext.Update) error { func sendDirHelp(ctx *ext.Context, update *ext.Update, userChatID int64) error {
args := strings.Split(strings.TrimPrefix(update.EffectiveMessage.Text, "/dir "), " ") dirs, err := dao.GetUserDirsByChatID(userChatID)
if len(args) < 3 { if err != nil {
dirs, err := dao.GetUserDirsByChatID(update.GetUserChat().GetID()) common.Log.Errorf("获取用户路径失败: %s", err)
if err != nil { ctx.Reply(update, ext.ReplyTextString("获取用户路径失败"), nil)
logger.L.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(" - 删除路径\n"),
styling.Plain("\n示例:\n"),
styling.Code("/dir add local1 path/to/dir"),
styling.Plain("\n\n当前已添加的路径:\n"),
styling.Blockquote(func() string {
var sb strings.Builder
for _, dir := range dirs {
sb.WriteString(dir.StorageName)
sb.WriteString(" - ")
sb.WriteString(dir.Path)
sb.WriteString("\n")
}
return sb.String()
}(), true),
},
), nil)
return dispatcher.EndGroups 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()) 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
} }
switch args[0] { switch args[1] {
case "add": case "add":
return addDir(ctx, update, user, args[1], args[2]) // /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": case "del":
return delDir(ctx, update, user, args[1], args[2]) // /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, dirID)
default: default:
ctx.Reply(update, ext.ReplyTextString("未知操作"), nil) ctx.Reply(update, ext.ReplyTextString("未知操作"), nil)
return dispatcher.EndGroups return dispatcher.EndGroups
@@ -69,7 +91,7 @@ func addDir(ctx *ext.Context, update *ext.Update, user *dao.User, storageName, p
} }
if err := dao.CreateDirForUser(user.ID, storageName, path); err != nil { if err := dao.CreateDirForUser(user.ID, storageName, path); 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
} }
@@ -77,9 +99,9 @@ func addDir(ctx *ext.Context, update *ext.Update, user *dao.User, storageName, p
return dispatcher.EndGroups return dispatcher.EndGroups
} }
func delDir(ctx *ext.Context, update *ext.Update, user *dao.User, storageName, path string) error { func delDir(ctx *ext.Context, update *ext.Update, dirID int) error {
if err := dao.DeleteDirForUser(user.ID, storageName, path); err != nil { if err := dao.DeleteDirByID(uint(dirID)); 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
} }

View File

@@ -6,14 +6,13 @@ 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/types" "github.com/krau/SaveAny-Bot/types"
) )
func handleFileMessage(ctx *ext.Context, update *ext.Update) error { func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
logger.L.Trace("Got media: ", update.EffectiveMessage.Media.TypeName()) common.Log.Trace("Got media: ", update.EffectiveMessage.Media.TypeName())
supported, err := supportedMediaFilter(update.EffectiveMessage.Message) supported, err := supportedMediaFilter(update.EffectiveMessage.Message)
if err != nil { if err != nil {
return err return err
@@ -24,30 +23,30 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
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
} }
storages := storage.GetUserStorages(user.ChatID) // storages := storage.GetUserStorages(user.ChatID)
if len(storages) == 0 { // if len(storages) == 0 {
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil) // ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
return dispatcher.EndGroups // return dispatcher.EndGroups
} // }
msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil) msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
if err != nil { if err != nil {
logger.L.Errorf("回复失败: %s", err) common.Log.Errorf("回复失败: %s", err)
return dispatcher.EndGroups return dispatcher.EndGroups
} }
media := update.EffectiveMessage.Media media := update.EffectiveMessage.Media
file, err := FileFromMedia(media, "") file, err := FileFromMedia(media, "")
if err != nil { if err != nil {
logger.L.Errorf("获取文件失败: %s", err) common.Log.Errorf("获取文件失败: %s", err)
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil) ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil)
return dispatcher.EndGroups return dispatcher.EndGroups
} }
if file.FileName == "" { if file.FileName == "" {
file.FileName = fmt.Sprintf("%d_%d_%s", update.EffectiveChat().GetID(), update.EffectiveMessage.ID, file.Hash()) file.FileName = GenFileNameFromMessage(*update.EffectiveMessage.Message, file)
} }
if err := dao.SaveReceivedFile(&dao.ReceivedFile{ if err := dao.SaveReceivedFile(&dao.ReceivedFile{
@@ -58,18 +57,18 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
ReplyMessageID: msg.ID, ReplyMessageID: msg.ID,
ReplyChatID: update.GetUserChat().GetID(), ReplyChatID: update.GetUserChat().GetID(),
}); err != nil { }); err != nil {
logger.L.Errorf("添加接收的文件失败: %s", err) common.Log.Errorf("添加接收的文件失败: %s", err)
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
Message: fmt.Sprintf("添加接收的文件失败: %s", err), Message: fmt.Sprintf("添加接收的文件失败: %s", err),
ID: msg.ID, ID: msg.ID,
}); err != nil { }); err != nil {
logger.L.Errorf("编辑消息失败: %s", err) common.Log.Errorf("编辑消息失败: %s", err)
} }
return dispatcher.EndGroups return dispatcher.EndGroups
} }
if !user.Silent || user.DefaultStorage == "" { if !user.Silent || user.DefaultStorage == "" {
return ProvideSelectMessage(ctx, update, file, update.EffectiveChat().GetID(), update.EffectiveMessage.ID, msg.ID) return ProvideSelectMessage(ctx, update, file.FileName, update.EffectiveChat().GetID(), update.EffectiveMessage.ID, msg.ID)
} }
return HandleSilentAddTask(ctx, update, user, &types.Task{ return HandleSilentAddTask(ctx, update, user, &types.Task{
Ctx: ctx, Ctx: ctx,

View File

@@ -9,9 +9,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/types" "github.com/krau/SaveAny-Bot/types"
) )
@@ -20,74 +19,89 @@ var (
linkRegex = regexp.MustCompile(linkRegexString) linkRegex = regexp.MustCompile(linkRegexString)
) )
func parseLink(ctx *ext.Context, link string) (chatID int64, messageID int, err error) {
strSlice := strings.Split(link, "/")
if len(strSlice) < 3 {
return 0, 0, fmt.Errorf("链接格式错误: %s", link)
}
messageID, err = strconv.Atoi(strSlice[len(strSlice)-1])
if err != nil {
return 0, 0, fmt.Errorf("无法解析消息 ID: %s", err)
}
if len(strSlice) == 3 {
chatUsername := strSlice[1]
linkChat, err := ctx.ResolveUsername(chatUsername)
if err != nil {
return 0, 0, fmt.Errorf("解析用户名失败: %s", err)
}
if linkChat == nil {
return 0, 0, fmt.Errorf("找不到该聊天: %s", chatUsername)
}
chatID = linkChat.GetID()
} else if len(strSlice) == 4 {
chatIDInt, err := strconv.Atoi(strSlice[2])
if err != nil {
return 0, 0, fmt.Errorf("无法解析 Chat ID: %s", err)
}
chatID = int64(chatIDInt)
} else {
return 0, 0, fmt.Errorf("无效的链接: %s", link)
}
return chatID, messageID, nil
}
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
} }
strSlice := strings.Split(link, "/") linkChatID, messageID, err := parseLink(ctx, link)
if len(strSlice) < 3 {
return dispatcher.ContinueGroups
}
messageID, err := strconv.Atoi(strSlice[2])
if err != nil { if err != nil {
logger.L.Errorf("解析消息 ID 失败: %s", err) common.Log.Errorf("解析链接失败: %s", err)
ctx.Reply(update, ext.ReplyTextString("无法解析消息 ID"), nil) ctx.Reply(update, ext.ReplyTextString("解析链接失败: "+err.Error()), nil)
return dispatcher.EndGroups
}
chatUsername := strSlice[1]
linkChat, err := ctx.ResolveUsername(chatUsername)
if err != nil {
logger.L.Errorf("解析 Chat ID 失败: %s", err)
ctx.Reply(update, ext.ReplyTextString("无法解析 Chat ID"), nil)
return dispatcher.EndGroups
}
if linkChat == nil {
logger.L.Errorf("无法找到聊天: %s", chatUsername)
ctx.Reply(update, ext.ReplyTextString("无法找到聊天"), nil)
return dispatcher.EndGroups 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
} }
storages := storage.GetUserStorages(user.ChatID)
if len(storages) == 0 { // storages := storage.GetUserStorages(user.ChatID)
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil) // if len(storages) == 0 {
return dispatcher.EndGroups // ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
} // return dispatcher.EndGroups
// }
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件..."), nil) replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件..."), nil)
if err != nil { if err != nil {
logger.L.Errorf("回复失败: %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 := &dao.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 +109,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 +117,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
View 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
}

View File

@@ -2,51 +2,76 @@ package bot
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"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/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"
) )
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 { func saveCmd(ctx *ext.Context, update *ext.Update) error {
res, ok := update.EffectiveMessage.GetReplyTo() args := strings.Split(update.EffectiveMessage.Text, " ")
if !ok || res == nil { if len(args) >= 3 {
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil) return handleBatchSave(ctx, update, args[1:])
return dispatcher.EndGroups
} }
replyHeader, ok := res.(*tg.MessageReplyHeader)
if !ok { replyToMsgID := func() int {
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil) res, ok := update.EffectiveMessage.GetReplyTo()
return dispatcher.EndGroups if !ok || res == nil {
} return 0
replyToMsgID, ok := replyHeader.GetReplyToMsgID() }
if !ok { replyHeader, ok := res.(*tg.MessageReplyHeader)
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil) if !ok {
return dispatcher.EndGroups 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()) 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
} }
storages := storage.GetUserStorages(user.ChatID) // storages := storage.GetUserStorages(user.ChatID)
// if len(storages) == 0 {
if len(storages) == 0 { // ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil) // return dispatcher.EndGroups
return dispatcher.EndGroups // }
}
msg, err := GetTGMessage(ctx, update.EffectiveChat().GetID(), replyToMsgID) msg, err := GetTGMessage(ctx, update.EffectiveChat().GetID(), replyToMsgID)
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
} }
@@ -59,7 +84,7 @@ func saveCmd(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
} }
@@ -68,7 +93,7 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
file, err := FileFromMessage(ctx, update.EffectiveChat().GetID(), msg.ID, customFileName) file, err := FileFromMessage(ctx, update.EffectiveChat().GetID(), msg.ID, customFileName)
if err != nil { if 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: fmt.Sprintf("获取文件失败: %s", err), Message: fmt.Sprintf("获取文件失败: %s", err),
ID: replied.ID, ID: replied.ID,
@@ -76,9 +101,8 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
return dispatcher.EndGroups return dispatcher.EndGroups
} }
// TODO: better file name
if file.FileName == "" { if file.FileName == "" {
file.FileName = fmt.Sprintf("%d_%d_%s", update.EffectiveChat().GetID(), replyToMsgID, file.Hash()) file.FileName = GenFileNameFromMessage(*msg, file)
} }
receivedFile := &dao.ReceivedFile{ receivedFile := &dao.ReceivedFile{
Processing: false, Processing: false,
@@ -90,17 +114,17 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
} }
if err := dao.SaveReceivedFile(receivedFile); err != nil { if err := dao.SaveReceivedFile(receivedFile); err != nil {
logger.L.Errorf("保存接收的文件失败: %s", err) common.Log.Errorf("保存接收的文件失败: %s", err)
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
Message: fmt.Sprintf("保存接收的文件失败: %s", err), Message: fmt.Sprintf("保存接收的文件失败: %s", err),
ID: replied.ID, ID: replied.ID,
}); err != nil { }); err != nil {
logger.L.Errorf("编辑消息失败: %s", err) common.Log.Errorf("编辑消息失败: %s", err)
} }
return dispatcher.EndGroups return dispatcher.EndGroups
} }
if !user.Silent || user.DefaultStorage == "" { if !user.Silent || user.DefaultStorage == "" {
return ProvideSelectMessage(ctx, update, file, update.EffectiveChat().GetID(), msg.ID, replied.ID) return ProvideSelectMessage(ctx, update, file.FileName, update.EffectiveChat().GetID(), msg.ID, replied.ID)
} }
return HandleSilentAddTask(ctx, update, user, &types.Task{ return HandleSilentAddTask(ctx, update, user, &types.Task{
Ctx: ctx, Ctx: ctx,
@@ -114,3 +138,125 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
UserID: user.ChatID, 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
}

95
bot/handle_send.go Normal file
View File

@@ -0,0 +1,95 @@
package bot
import (
"fmt"
"strconv"
"strings"
"github.com/celestix/gotgproto/dispatcher"
"github.com/celestix/gotgproto/ext"
tgtypes "github.com/celestix/gotgproto/types"
"github.com/gotd/td/tg"
)
func copyMediaToChat(ctx *ext.Context, msg *tg.Message, chatID int64) (*tgtypes.Message, error) {
media, ok := msg.GetMedia()
if !ok {
return nil, fmt.Errorf("获取媒体失败")
}
req := &tg.MessagesSendMediaRequest{
InvertMedia: msg.InvertMedia,
Message: msg.Message,
}
switch m := media.(type) {
case *tg.MessageMediaDocument:
document, ok := m.Document.AsNotEmpty()
if !ok {
return nil, ErrEmptyDocument
}
inputMedia := &tg.InputMediaDocument{
ID: document.AsInput(),
}
inputMedia.SetFlags()
req.Media = inputMedia
case *tg.MessageMediaPhoto:
photo, ok := m.Photo.AsNotEmpty()
if !ok {
return nil, ErrEmptyPhoto
}
inputMedia := &tg.InputMediaPhoto{
ID: photo.AsInput(),
}
inputMedia.SetFlags()
req.Media = inputMedia
default:
return nil, fmt.Errorf("不支持的媒体类型: %T", media)
}
req.SetEntities(msg.Entities)
req.SetFlags()
return ctx.SendMedia(chatID, req)
}
func sendFileToTelegram(ctx *ext.Context, update *ext.Update) error {
args := strings.Split(string(update.CallbackQuery.Data), " ")
if len(args) < 3 {
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID,
Alert: true,
Message: "参数错误",
CacheTime: 5,
})
return dispatcher.EndGroups
}
fileChatID, _ := strconv.Atoi(args[1])
fileMessageID, _ := strconv.Atoi(args[2])
fileMessage, err := GetTGMessage(ctx, int64(fileChatID), fileMessageID)
if err != nil {
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID,
Alert: true,
Message: "无法获取文件消息",
CacheTime: 5,
})
return dispatcher.EndGroups
}
_, err = copyMediaToChat(ctx, fileMessage, update.EffectiveChat().GetID())
if err != nil {
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID,
Alert: true,
Message: fmt.Sprintf("发送文件失败: %s", err),
CacheTime: 5,
})
} else {
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID,
})
}
return dispatcher.EndGroups
}

View File

@@ -5,14 +5,14 @@ import (
"github.com/celestix/gotgproto/dispatcher" "github.com/celestix/gotgproto/dispatcher"
"github.com/celestix/gotgproto/ext" "github.com/celestix/gotgproto/ext"
"github.com/krau/SaveAny-Bot/common"
"github.com/krau/SaveAny-Bot/dao" "github.com/krau/SaveAny-Bot/dao"
"github.com/krau/SaveAny-Bot/logger"
) )
func silent(ctx *ext.Context, update *ext.Update) error { func silent(ctx *ext.Context, update *ext.Update) error {
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)
return dispatcher.EndGroups return dispatcher.EndGroups
} }
if !user.Silent && user.DefaultStorage == "" { if !user.Silent && user.DefaultStorage == "" {
@@ -21,7 +21,7 @@ func silent(ctx *ext.Context, update *ext.Update) error {
} }
user.Silent = !user.Silent user.Silent = !user.Silent
if err := dao.UpdateUser(user); err != nil { if err := dao.UpdateUser(user); err != nil {
logger.L.Errorf("更新用户失败: %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
} }

View File

@@ -1,15 +1,17 @@
package bot package bot
import ( import (
"fmt"
"github.com/celestix/gotgproto/dispatcher" "github.com/celestix/gotgproto/dispatcher"
"github.com/celestix/gotgproto/ext" "github.com/celestix/gotgproto/ext"
"github.com/krau/SaveAny-Bot/common"
"github.com/krau/SaveAny-Bot/dao" "github.com/krau/SaveAny-Bot/dao"
"github.com/krau/SaveAny-Bot/logger"
) )
func start(ctx *ext.Context, update *ext.Update) error { func start(ctx *ext.Context, update *ext.Update) error {
if err := dao.CreateUser(update.GetUserChat().GetID()); err != nil { if err := dao.CreateUser(update.GetUserChat().GetID()); err != nil {
logger.L.Errorf("创建用户失败: %s", err) common.Log.Errorf("创建用户失败: %s", err)
return dispatcher.EndGroups return dispatcher.EndGroups
} }
return help(ctx, update) return help(ctx, update)
@@ -17,6 +19,7 @@ func start(ctx *ext.Context, update *ext.Update) error {
const helpText string = ` const helpText string = `
Save Any Bot - 转存你的 Telegram 文件 Save Any Bot - 转存你的 Telegram 文件
版本: %s , 提交: %s
命令: 命令:
/start - 开始使用 /start - 开始使用
/help - 显示帮助 /help - 显示帮助
@@ -32,6 +35,6 @@ Save Any Bot - 转存你的 Telegram 文件
` `
func help(ctx *ext.Context, update *ext.Update) error { func help(ctx *ext.Context, update *ext.Update) error {
ctx.Reply(update, ext.ReplyTextString(helpText), nil) ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf(helpText, common.Version, common.GitCommit[:7])), nil)
return dispatcher.EndGroups return dispatcher.EndGroups
} }

View File

@@ -8,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"
) )
@@ -22,7 +22,7 @@ func storageCmd(ctx *ext.Context, update *ext.Update) error {
} }
markup, err := getSetDefaultStorageMarkup(userChatID, storages) markup, err := getSetDefaultStorageMarkup(userChatID, storages)
if err != nil { if err != nil {
logger.L.Errorf("Failed to get markup: %s", err) common.Log.Errorf("Failed to get markup: %s", err)
ctx.Reply(update, ext.ReplyTextString("获取存储位置失败"), nil) ctx.Reply(update, ext.ReplyTextString("获取存储位置失败"), nil)
return dispatcher.EndGroups return dispatcher.EndGroups
} }
@@ -47,7 +47,7 @@ func setDefaultStorage(ctx *ext.Context, update *ext.Update) error {
cbDataId, _ := strconv.Atoi(args[2]) cbDataId, _ := strconv.Atoi(args[2])
storageName, err := dao.GetCallbackData(uint(cbDataId)) storageName, err := dao.GetCallbackData(uint(cbDataId))
if err != nil { if err != nil {
logger.L.Errorf("获取回调数据失败: %s", err) common.Log.Errorf("获取回调数据失败: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,
@@ -60,7 +60,7 @@ func setDefaultStorage(ctx *ext.Context, update *ext.Update) error {
selectedStorage, err := storage.GetStorageByName(storageName) selectedStorage, err := storage.GetStorageByName(storageName)
if err != nil { if err != nil {
logger.L.Errorf("获取指定存储失败: %s", err) common.Log.Errorf("获取指定存储失败: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,
@@ -71,7 +71,7 @@ func setDefaultStorage(ctx *ext.Context, update *ext.Update) error {
} }
user, err := dao.GetUserByChatID(int64(userID)) user, err := dao.GetUserByChatID(int64(userID))
if err != nil { if err != nil {
logger.L.Errorf("Failed to get user: %s", err) common.Log.Errorf("Failed to get user: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,
@@ -82,7 +82,7 @@ func setDefaultStorage(ctx *ext.Context, update *ext.Update) error {
} }
user.DefaultStorage = storageName user.DefaultStorage = storageName
if err := dao.UpdateUser(user); err != nil { if err := dao.UpdateUser(user); err != nil {
logger.L.Errorf("Failed to update user: %s", err) common.Log.Errorf("Failed to update user: %s", err)
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{ ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID, QueryID: update.CallbackQuery.QueryID,
Alert: true, Alert: true,

114
bot/handle_telegraph.go Normal file
View 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,
})
}

View File

@@ -4,7 +4,7 @@ import (
"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/krau/SaveAny-Bot/logger" "github.com/krau/SaveAny-Bot/common"
) )
func RegisterHandlers(dispatcher dispatcher.Dispatcher) { func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
@@ -15,13 +15,20 @@ func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
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("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.NewCallbackQuery(filters.CallbackQuery.Prefix("cancel"), cancelTask))
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("send_here"), sendFileToTelegram))
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage)) dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
} }

View File

@@ -9,12 +9,13 @@ import (
"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/common"
"github.com/krau/SaveAny-Bot/config" "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,
@@ -30,8 +31,40 @@ const noPermissionText string = `
func checkPermission(ctx *ext.Context, update *ext.Update) error { func checkPermission(ctx *ext.Context, update *ext.Update) error {
userID := update.GetUserChat().GetID() userID := update.GetUserChat().GetID()
if !slice.Contain(config.Cfg.GetUsersID(), userID) { if !slice.Contain(config.Cfg.GetUsersID(), userID) {
if config.Cfg.AsPublicCopyMediaBot {
tryCopyMedia(ctx, update)
return dispatcher.EndGroups
}
ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil) ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil)
return dispatcher.EndGroups return dispatcher.EndGroups
} }
return dispatcher.ContinueGroups return dispatcher.ContinueGroups
} }
func tryCopyMedia(ctx *ext.Context, update *ext.Update) {
if !config.Cfg.AsPublicCopyMediaBot {
return
}
if update.EffectiveMessage == nil || update.EffectiveMessage.Message == nil {
return
}
common.Log.Tracef("Got copy media request from %d", update.EffectiveChat().GetID())
msg := update.EffectiveMessage.Message
if link := linkRegex.FindString(update.EffectiveMessage.Text); link != "" {
linkChatID, messageID, err := parseLink(ctx, link)
if err != nil {
return
}
fileMessage, err := GetTGMessage(ctx, linkChatID, messageID)
if err != nil {
return
}
if fileMessage == nil || fileMessage.Media == nil {
return
}
msg = fileMessage
}
if _, err := copyMediaToChat(ctx, msg, update.EffectiveChat().GetID()); err != nil {
common.Log.Errorf("Failed to copy media: %v", err)
}
}

View File

@@ -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"
@@ -47,9 +49,9 @@ func getSelectStorageMarkup(userChatID int64, fileChatID, fileMessageID int) (*t
return nil, fmt.Errorf("failed to get user by chat ID: %d, error: %w", userChatID, 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 { // if len(storages) == 0 {
return nil, ErrNoStorages // return nil, ErrNoStorages
} // }
buttons := make([]tg.KeyboardButtonClass, 0) buttons := make([]tg.KeyboardButtonClass, 0)
for _, storage := range storages { for _, storage := range storages {
@@ -60,7 +62,7 @@ func getSelectStorageMarkup(userChatID int64, fileChatID, fileMessageID int) (*t
} }
buttons = append(buttons, &tg.KeyboardButtonCallback{ buttons = append(buttons, &tg.KeyboardButtonCallback{
Text: storage.Name(), Text: storage.Name(),
Data: []byte(fmt.Sprintf("add %d", cbDataId)), Data: fmt.Appendf(nil, "add %d", cbDataId),
}) })
} }
markup := &tg.ReplyInlineMarkup{} markup := &tg.ReplyInlineMarkup{}
@@ -69,6 +71,14 @@ func getSelectStorageMarkup(userChatID int64, fileChatID, fileMessageID int) (*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)
} }
markup.Rows = append(markup.Rows, tg.KeyboardButtonRow{
Buttons: []tg.KeyboardButtonClass{
&tg.KeyboardButtonCallback{
Text: "发送到当前聊天",
Data: []byte(fmt.Sprintf("send_here %d %d", fileChatID, fileMessageID)),
},
},
})
return markup, nil return markup, nil
} }
@@ -178,12 +188,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
@@ -192,14 +204,19 @@ 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:%d", chatId, 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
@@ -212,32 +229,35 @@ func GetTGMessage(ctx *ext.Context, chatId int64, messageID int) (*tg.Message, e
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.GetUserChat().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,
@@ -251,7 +271,7 @@ 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
} }
@@ -271,3 +291,50 @@ func HandleSilentAddTask(ctx *ext.Context, update *ext.Update, user *dao.User, t
}) })
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
}

View File

@@ -7,12 +7,13 @@ import (
"path/filepath" "path/filepath"
"syscall" "syscall"
"slices"
"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/core" "github.com/krau/SaveAny-Bot/core"
"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/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -24,32 +25,30 @@ func Run(_ *cobra.Command, _ []string) {
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit sig := <-quit
logger.L.Info(sig, ", 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 := common.RemoveAllInDir(cachePath); err != nil {
logger.L.Error("Failed to clean cache dir: ", err) common.Log.Error("清理缓存失败: ", err)
} }
} }
} }
@@ -59,8 +58,8 @@ func InitAll() {
fmt.Println("加载配置文件失败: ", err) fmt.Println("加载配置文件失败: ", err)
os.Exit(1) os.Exit(1)
} }
logger.InitLogger() common.InitLogger()
logger.L.Info("正在启动 SaveAny-Bot...") common.Log.Info("正在启动 SaveAny-Bot...")
dao.Init() dao.Init()
storage.LoadStorages() storage.LoadStorages()
common.Init() common.Init()

View File

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

View File

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

View File

@@ -1,57 +1,36 @@
package common package common
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"github.com/krau/SaveAny-Bot/logger"
) )
// 创建文件, 自动创建目录
func MkFile(path string, data []byte) error {
err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
if err != nil {
return err
}
return os.WriteFile(path, data, os.ModePerm)
}
// 删除文件, 并清理空目录. 如果文件不存在则返回 nil
func PurgeFile(path string) error {
if err := os.Remove(path); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
}
return RemoveEmptyDirectories(filepath.Dir(path))
}
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) if err := os.Remove(path); err != nil {
Log.Errorf("Failed to remove file %s: %s", path, err)
}
}) })
} }
// 递归删除目录 // 删除目录下的所有内容, 但不删除目录本身
func RemoveEmptyDirectories(dirPath string) error { func RemoveAllInDir(dirPath string) error {
entries, err := os.ReadDir(dirPath) entries, err := os.ReadDir(dirPath)
if err != nil { if err != nil {
return err return err
} }
if len(entries) == 0 { for _, entry := range entries {
err := os.Remove(dirPath) entryPath := filepath.Join(dirPath, entry.Name())
if err != nil { if err := os.RemoveAll(entryPath); err != nil {
return err return err
} }
return RemoveEmptyDirectories(filepath.Dir(dirPath))
} }
return nil return nil
} }

View File

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

View File

@@ -10,32 +10,46 @@ stream = false # 使用stream模式, 详情请查看文档
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"
@@ -44,12 +58,13 @@ base_path = '/'
url = 'https://alist.com' url = 'https://alist.com'
username = 'admin' username = 'admin'
password = 'password' password = 'password'
token_exp = 86400 # 86400--1天 604800--7天 1296000--15天 2592000--30天 15552000--180天 # 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"
@@ -59,23 +74,16 @@ url = 'https://example.com/dav'
username = 'username' username = 'username'
password = 'password' password = 'password'
[[storages]]
# 用户列表 name = "MyMinio"
[[users]] type = "minio"
# telegram user id enable = true
id = 114514 endpoint = 'play.min.io'
# 开启黑名单,开启后下方留空以使用所有存储,反之则为白名单,白名单请在下方输入允许的存储名 use_ssl = true
blacklist = true access_key_id = 'Q3AM3UQ867SPQQA43P2F'
# 将列表留空并开启黑名单模式以允许使用所有存储此处示例为黑名单模式用户114514 可使用所有存储 secret_access_key = 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG'
storages = [] bucket_name = 'saveanybot'
base_path = '/path/telegram'
[[users]]
id = 123456
blacklist = false #开启白名单模式此时用户123456 仅可使用下方列表中的存储
# 此时该用户只能使用名为 本机1 的存储
storages = ["本机1"]
# 其他配置 # 其他配置
@@ -91,4 +99,6 @@ storages = ["本机1"]
# cache_ttl = 30 # cache_ttl = 30
# [db] # [db]
# path = "data/data.db" # 数据库文件路径 # path = "data/data.db" # 数据库文件路径
# session = "data/session.db"

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,8 @@ 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"
) )
@@ -15,15 +17,16 @@ type Config struct {
Threads int `toml:"threads" mapstructure:"threads" json:"threads"` Threads int `toml:"threads" mapstructure:"threads" json:"threads"`
Stream bool `toml:"stream" mapstructure:"stream" json:"stream"` Stream bool `toml:"stream" mapstructure:"stream" json:"stream"`
// Experimental: 将拷贝媒体文件的功能设为公开可用
AsPublicCopyMediaBot bool `toml:"as_public_copy_media_bot" mapstructure:"as_public_copy_media_bot" json:"as_public_copy_media_bot"`
Users []userConfig `toml:"users" mapstructure:"users" json:"users"` Users []userConfig `toml:"users" mapstructure:"users" json:"users"`
Temp tempConfig `toml:"temp" mapstructure:"temp"` Temp tempConfig `toml:"temp" mapstructure:"temp"`
Log logConfig `toml:"log" mapstructure:"log"` Log logConfig `toml:"log" mapstructure:"log"`
DB dbConfig `toml:"db" mapstructure:"db"` DB dbConfig `toml:"db" mapstructure:"db"`
Telegram telegramConfig `toml:"telegram" mapstructure:"telegram"` Telegram telegramConfig `toml:"telegram" mapstructure:"telegram"`
Storages []StorageConfig `toml:"-" mapstructure:"-" json:"storages"` Storages []storage.StorageConfig `toml:"-" mapstructure:"-" json:"storages"`
// Deprecated
DeprecatedStorage deprecatedStorageConfig `toml:"storage" mapstructure:"storage"`
} }
type tempConfig struct { type tempConfig struct {
@@ -38,17 +41,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 {
@@ -58,6 +63,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(".")
@@ -74,15 +88,18 @@ func Init() error {
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 {
@@ -102,38 +119,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 {
@@ -151,6 +142,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
} }

View File

@@ -6,28 +6,35 @@ import (
"fmt" "fmt"
"github.com/celestix/gotgproto/ext" "github.com/celestix/gotgproto/ext"
"github.com/gotd/td/telegram/downloader"
"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"
"github.com/krau/SaveAny-Bot/queue" "github.com/krau/SaveAny-Bot/queue"
"github.com/krau/SaveAny-Bot/types" "github.com/krau/SaveAny-Bot/types"
) )
var Downloader *downloader.Downloader
func init() {
Downloader = downloader.NewDownloader().WithPartSize(1024 * 1024)
}
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 {
task.Error = err task.Error = err
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
task.Status = types.Canceled task.Status = types.Canceled
} else { } else {
logger.L.Errorf("Failed to do task: %s", err) common.Log.Errorf("Failed to do task: %s", err)
task.Status = types.Failed task.Status = types.Failed
} }
} else { } else {
@@ -35,49 +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())
extCtx, ok := task.Ctx.(*ext.Context) extCtx, ok := task.Ctx.(*ext.Context)
if !ok { if !ok {
logger.L.Errorf("Context is not *ext.Context: %T", task.Ctx) common.Log.Errorf("Context is not *ext.Context: %T", task.Ctx)
} else { } else if task.ReplyMessageID != 0 {
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Message: fmt.Sprintf("文件保存成功\n [%s]: %s", task.StorageName, task.StoragePath), Message: fmt.Sprintf("文件保存成功\n [%s]: %s", task.StorageName, task.StoragePath),
ID: task.ReplyMessageID, ID: task.ReplyMessageID,
}) })
} }
case types.Failed: case types.Failed:
logger.L.Errorf("Task failed: %s", task.String()) common.Log.Errorf("Task failed: %s", task.String())
extCtx, ok := task.Ctx.(*ext.Context) extCtx, ok := task.Ctx.(*ext.Context)
if !ok { if !ok {
logger.L.Errorf("Context is not *ext.Context: %T", task.Ctx) common.Log.Errorf("Context is not *ext.Context: %T", task.Ctx)
} else { } else if task.ReplyMessageID != 0 {
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Message: "文件保存失败\n" + task.Error.Error(), Message: "文件保存失败\n" + task.Error.Error(),
ID: task.ReplyMessageID, 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) extCtx, ok := task.Ctx.(*ext.Context)
if !ok { if !ok {
logger.L.Errorf("Context is not *ext.Context: %T", task.Ctx) common.Log.Errorf("Context is not *ext.Context: %T", task.Ctx)
} else { } else if task.ReplyMessageID != 0 {
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Message: "任务已取消", Message: "任务已取消",
ID: task.ReplyMessageID, 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; status: %s", task.String(), task.Status) common.Log.Debugf("Task done: %s; status: %s", task.String(), task.Status)
queue.DoneTask(task) 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)

View File

@@ -2,47 +2,50 @@ package core
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"path"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/celestix/gotgproto/ext" "github.com/celestix/gotgproto/ext"
"github.com/celestix/telegraph-go/v2"
"github.com/duke-git/lancet/v2/fileutil" "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/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/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"
"golang.org/x/sync/errgroup"
) )
func processPendingTask(task *types.Task) error { func processPendingTask(task *types.Task) error {
logger.L.Debugf("Start processing task: %s", task.String()) common.Log.Debugf("Start processing task: %s", task.String())
if task.FileName() != "" && !task.IsTelegraph && task.File.FileSize != 0 && task.FileDBID != 0 {
ext := path.Ext(task.FileName())
name := task.FileName()[:len(task.FileName())-len(ext)]
task.File.FileName = fmt.Sprintf("%s_%d%s", name, task.FileDBID, ext)
}
if task.FileName() == "" { if task.FileName() == "" {
task.File.FileName = fmt.Sprintf("%d_%d_%s", task.FileChatID, task.FileMessageID, task.File.Hash()) 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 == "" { taskStorage, storagePath, err := getStorageAndPathForTask(task)
task.StoragePath = task.File.FileName
}
taskStorage, err := storage.GetStorageByUserIDAndName(task.UserID, task.StorageName)
if err != nil { if err != nil {
return err return err
} }
task.StoragePath = taskStorage.JoinStoragePath(*task) if taskStorage == nil {
return fmt.Errorf("not found storage: %s", task.StorageName)
if task.File.FileSize == 0 {
return processPhoto(task, taskStorage, cacheDestPath)
} }
task.StoragePath = storagePath
ctx, ok := task.Ctx.(*ext.Context) ctx, ok := task.Ctx.(*ext.Context)
if !ok { if !ok {
@@ -52,47 +55,86 @@ func processPendingTask(task *types.Task) error {
cancelCtx, cancel := context.WithCancel(ctx) cancelCtx, cancel := context.WithCancel(ctx)
task.Cancel = cancel task.Cancel = cancel
downloadBuider := Downloader.Download(bot.Client.API(), task.File.Location).WithThreads(getTaskThreads(task.File.FileSize)) 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)
taskStreamStorage, isStreamStorage := taskStorage.(storage.StreamStorage)
if config.Cfg.Stream { if config.Cfg.Stream {
if !isStreamStorage { if !notsupportStream {
logger.L.Warnf("存储 %s 不支持流式上传", taskStorage.Name())
} else {
text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0) text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0)
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ if task.ReplyMessageID != 0 {
Message: text, ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Entities: entities, Message: text,
ID: task.ReplyMessageID, Entities: entities,
ReplyMarkup: getCancelTaskMarkup(task), ID: task.ReplyMessageID,
}) ReplyMarkup: cancelMarkUp,
uploadStream, err := taskStreamStorage.NewUploadStream(cancelCtx, task.StoragePath) })
if err != nil {
return fmt.Errorf("创建上传流失败: %w", err)
} }
defer uploadStream.Close()
pr, pw := io.Pipe()
defer pr.Close()
task.StartTime = time.Now() task.StartTime = time.Now()
progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize)) progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize))
progressStream := NewProgressStream(uploadStream, task.File.FileSize, progressCallback) progressStream := NewProgressStream(pw, task.File.FileSize, progressCallback)
_, err = downloadBuider.Stream(cancelCtx, progressStream) eg, uploadCtx := errgroup.WithContext(cancelCtx)
if err != nil {
return fmt.Errorf("下载文件失败: %w", err) 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
} }
logger.L.Infof("Uploaded file: %s", task.StoragePath)
return nil 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) text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0)
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ if task.ReplyMessageID != 0 {
Message: text, ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Entities: entities, Message: text,
ID: task.ReplyMessageID, Entities: entities,
ReplyMarkup: getCancelTaskMarkup(task), ID: task.ReplyMessageID,
}) ReplyMarkup: cancelMarkUp,
})
}
progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize)) progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize))
dest, err := NewTaskLocalFile(cacheDestPath, task.File.FileSize, progressCallback) dest, err := NewTaskLocalFile(cacheDestPath, task.File.FileSize, progressCallback)
@@ -101,7 +143,7 @@ func processPendingTask(task *types.Task) error {
} }
defer dest.Close() defer dest.Close()
task.StartTime = time.Now() task.StartTime = time.Now()
_, err = downloadBuider.Parallel(cancelCtx, dest) _, err = downloadBuilder.Parallel(cancelCtx, dest)
if err != nil { if err != nil {
return fmt.Errorf("下载文件失败: %w", err) return fmt.Errorf("下载文件失败: %w", err)
} }
@@ -109,11 +151,143 @@ func processPendingTask(task *types.Task) error {
fixTaskFileExt(task, cacheDestPath) fixTaskFileExt(task, cacheDestPath)
logger.L.Infof("Downloaded file: %s", cacheDestPath) common.Log.Infof("Downloaded file: %s", cacheDestPath)
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ if task.ReplyMessageID != 0 {
Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()), ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
ID: task.ReplyMessageID, Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()),
}) ID: task.ReplyMessageID,
})
return saveFileWithRetry(cancelCtx, task, taskStorage, cacheDestPath) }
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
View 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 %vgot %v", expected, got)
}
}

View File

@@ -1,9 +0,0 @@
package core
import "github.com/gotd/td/telegram/downloader"
var Downloader *downloader.Downloader
func init() {
Downloader = downloader.NewDownloader().WithPartSize(1024 * 1024)
}

103
core/rule.go Normal file
View 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)
}

View File

@@ -1,7 +1,9 @@
package core package core
import ( import (
"bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -9,6 +11,7 @@ import (
"time" "time"
"github.com/celestix/gotgproto/ext" "github.com/celestix/gotgproto/ext"
"github.com/celestix/telegraph-go/v2"
"github.com/gabriel-vasile/mimetype" "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"
@@ -16,24 +19,38 @@ import (
"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(ctx context.Context, 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 := ctx.Err(); err != nil { if err := vctx.Err(); err != nil {
return fmt.Errorf("context canceled while saving file: %w", err) return fmt.Errorf("context canceled while saving file: %w", err)
} }
if err := taskStorage.Save(ctx, localFilePath, task.StoragePath); err != nil { 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 { select {
case <-ctx.Done(): case <-vctx.Done():
return fmt.Errorf("context canceled during retry delay: %w", ctx.Err()) return fmt.Errorf("context canceled during retry delay: %w", vctx.Err())
case <-time.After(time.Duration(i*500) * time.Millisecond): case <-time.After(time.Duration(i*500) * time.Millisecond):
} }
continue continue
@@ -43,7 +60,7 @@ func saveFileWithRetry(ctx context.Context, task *types.Task, taskStorage storag
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,
@@ -58,15 +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.Ctx, task, taskStorage, cachePath)
} }
func cleanCacheFile(destPath string) { func cleanCacheFile(destPath string) {
@@ -74,7 +85,7 @@ 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)
} }
} }
} }
@@ -120,7 +131,7 @@ func buildProgressMessageEntity(task *types.Task, bytesRead int64, startTime tim
styling.Plain("\n当前进度: "), styling.Plain("\n当前进度: "),
styling.Bold(fmt.Sprintf("%.2f%%", 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()
@@ -129,11 +140,14 @@ func buildProgressMessageEntity(task *types.Task, bytesRead int64, startTime tim
func buildProgressCallback(ctx *ext.Context, task *types.Task, updateCount int) func(bytesRead, contentLength int64) { func buildProgressCallback(ctx *ext.Context, task *types.Task, updateCount int) func(bytesRead, contentLength int64) {
return func(bytesRead, contentLength int64) { return func(bytesRead, contentLength int64) {
progress := float64(bytesRead) / float64(contentLength) * 100 progress := float64(bytesRead) / float64(contentLength) * 100
logger.L.Tracef("Downloading %s: %.2f%%", task.String(), progress) common.Log.Tracef("Downloading %s: %.2f%%", task.String(), progress)
progressInt := int(progress) progressInt := int(progress)
if task.File.FileSize < 1024*1024*50 || progressInt == 0 || progressInt%int(100/updateCount) != 0 { if task.File.FileSize < 1024*1024*50 || progressInt == 0 || progressInt%int(100/updateCount) != 0 {
return return
} }
if task.ReplyMessageID == 0 {
return
}
text, entities := buildProgressMessageEntity(task, bytesRead, task.StartTime, progress) text, entities := buildProgressMessageEntity(task, bytesRead, task.StartTime, progress)
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Message: text, Message: text,
@@ -154,7 +168,7 @@ func fixTaskFileExt(task *types.Task, localFilePath string) {
if path.Ext(task.FileName()) == "" { if path.Ext(task.FileName()) == "" {
mimeType, err := mimetype.DetectFile(localFilePath) mimeType, err := mimetype.DetectFile(localFilePath)
if err != nil { if err != nil {
logger.L.Errorf("Failed to detect mime type: %s", err) common.Log.Errorf("Failed to detect mime type: %s", err)
} else { } else {
task.File.FileName = fmt.Sprintf("%s%s", task.FileName(), mimeType.Extension()) task.File.FileName = fmt.Sprintf("%s%s", task.FileName(), mimeType.Extension())
task.StoragePath = fmt.Sprintf("%s%s", task.StoragePath, mimeType.Extension()) task.StoragePath = fmt.Sprintf("%s%s", task.StoragePath, mimeType.Extension())
@@ -258,3 +272,27 @@ func NewProgressStream(writer io.Writer, size int64, callback func(bytesRead, co
interval: 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
}

View File

@@ -1,14 +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"
"gorm.io/gorm" "gorm.io/gorm"
glogger "gorm.io/gorm/logger" glogger "gorm.io/gorm/logger"
) )
@@ -17,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,
@@ -32,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(&ReceivedFile{}, &User{}, &Dir{}, &CallbackData{}); 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 {
@@ -66,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)
} }
} }
@@ -75,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")
}
}
}

View File

@@ -41,3 +41,7 @@ func GetDirsByUserIDAndStorageName(userID uint, storageName string) ([]Dir, erro
func DeleteDirForUser(userID uint, storageName, path string) error { 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 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
}

View File

@@ -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 {
@@ -22,6 +24,8 @@ type User struct {
Silent bool Silent bool
DefaultStorage string // Default storage name DefaultStorage string // Default storage name
Dirs []Dir Dirs []Dir
ApplyRule bool
Rules []Rule
} }
type Dir struct { type Dir struct {
@@ -35,3 +39,12 @@ type CallbackData struct {
gorm.Model gorm.Model
Data string Data string
} }
type Rule struct {
gorm.Model
UserID uint
Type string
Data string
StorageName string
DirPath string
}

22
dao/rule.go Normal file
View 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
}

View File

@@ -9,7 +9,9 @@ func CreateUser(chatID int64) error {
func GetAllUsers() ([]User, error) { func GetAllUsers() ([]User, error) {
var users []User var users []User
err := db.Preload("Dirs").Find(&users).Error err := db.Preload("Dirs").
Preload("Rules").
Find(&users).Error
return users, err return users, err
} }
@@ -17,6 +19,7 @@ func GetUserByChatID(chatID int64) (*User, error) {
var user User var user User
err := db. err := db.
Preload("Dirs"). Preload("Dirs").
Preload("Rules").
Where("chat_id = ?", chatID).First(&user).Error Where("chat_id = ?", chatID).First(&user).Error
return &user, err return &user, err
} }
@@ -26,5 +29,5 @@ func UpdateUser(user *User) error {
} }
func DeleteUser(user *User) error { func DeleteUser(user *User) error {
return db.Unscoped().Select("Dirs").Delete(user).Error return db.Unscoped().Select("Dirs", "Rules").Delete(user).Error
} }

13
docker-compose.local.yml Normal file
View File

@@ -0,0 +1,13 @@
services:
saveany-bot:
build: .
container_name: saveany-bot
restart: unless-stopped
volumes:
- ./data:/app/data
- ./config.toml:/app/config.toml
- ./downloads:/app/downloads
- ./cache:/app/cache
# 使用 host 模式以便访问宿主机服务 (如代理)
# 如果你对 Docker 网络模式熟悉, 可以自行修改
network_mode: host

11
docs/docs/contribute.md Normal file
View 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` 文件, 添加新的示例配置
*可能确实有点麻烦了 = =*

View File

@@ -4,7 +4,7 @@
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件. 在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
在解压后目录新建 `config.toml` 文件, 参考 [config.example.toml](./config.example.toml) 编辑配置文件. 在解压后目录新建 `config.toml` 文件, 参考 [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
运行: 运行:
@@ -40,7 +40,7 @@ systemctl enable --now saveany-bot
### 为OpenWrt及衍生系统添加开机自启动服务 ### 为OpenWrt及衍生系统添加开机自启动服务
创建文件 ` /etc/init.d/saveanybot` ,参考[saveanybot](./docs/saveanybot)自行修改. 创建文件 ` /etc/init.d/saveanybot` ,参考[saveanybot](https://github.com/krau/SaveAny-Bot/blob/main/docs/saveanybot)自行修改.
`chmod +x /etc/init.d/saveanybot` `chmod +x /etc/init.d/saveanybot`
@@ -50,7 +50,7 @@ systemctl enable --now saveany-bot
### 为OpenWrt及衍生系统添加快捷指令 ### 为OpenWrt及衍生系统添加快捷指令
创建文件` /usr/bin/sabot` ,参考[sabot](./docs/sabot)自行配置修改,注意此处文件编码仅支持 ANSI 936 . 创建文件` /usr/bin/sabot` ,参考[sabot](https://github.com/krau/SaveAny-Bot/blob/main/docs/sabot)自行配置修改,注意此处文件编码仅支持 ANSI 936 .
`chmod +x /usr/bin/sabot` `chmod +x /usr/bin/sabot`
@@ -61,7 +61,7 @@ systemctl enable --now saveany-bot
### Docker Compose ### Docker Compose
下载 [docker-compose.yml](./docker-compose.yml) 文件, 在同目录下新建 `config.toml` 文件, 参考 [config.example.toml](./config.example.toml) 编辑配置文件. 下载 [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) 编辑配置文件.
启动: 启动:

46
docs/docs/experimental.md Normal file
View File

@@ -0,0 +1,46 @@
# 实验性功能
这里的功能不太稳定, 且未来可能会被删除或修改。
## 存储规则
允许你为 Bot 在上传文件到存储时设置一些重定向规则, 用于自动整理所保存的文件.
见: https://github.com/krau/SaveAny-Bot/issues/28
目前支持的规则类型:
1. FILENAME-REGEX
2. MESSAGE-REGEX
添加规则的基本语法:
"规则类型 规则内容 存储名 路径"
注意空格的使用, 语法正确 bot 才能解析, 以下是一条合法的添加规则命令:
```
/rule add FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /视频
```
此外, 规则中的存储名若使用 "CHOSEN" , 则表示存储到点击按钮选择的存储端的路径下
规则介绍:
### FILENAME-REGEX
根据文件名正则匹配, 规则内容要求为一个合法的正则表达式, 如
```
FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /视频
```
表示将文件名后缀为 mp4,mkv,ts,avi,flv 的文件放到名为 MyAlist 存储下的 /视频 目录内 (同时受配置文件中的 `base_path` 影响)
### MESSAGE-REGEX
同上, 根据消息文本内容正则匹配
## 复制并发送媒体消息
将接收到的文件(媒体)消息, 或链接对应的消息原样发送到当前聊天, 点击选择存储按钮中的 "发送到当前聊天" 即可.

View File

@@ -2,15 +2,11 @@
## 上传 alist 失败也会显示成功 ## 上传 alist 失败也会显示成功
这是 alist 的上传实现导致的问题, 上传到 alist 的文件实际上会被 alist 暂存在本地, 在客户端上传结束后 alist 就返回成功, 然后 alist 会在后台将文件上传到对应的存储.
目前 bot 是根据 alist 的返回判断是否成功, 无法获知 alist 的后台上传任务是否成功.
在 alist 管理页面适当调整上传分片大小, 为 alist 使用更稳定的网络环境部署, 都可以减少这种情况的发生. 在 alist 管理页面适当调整上传分片大小, 为 alist 使用更稳定的网络环境部署, 都可以减少这种情况的发生.
## Bot 提示下载成功但是 alist 未显示 ## Bot 提示下载成功但是 alist 未显示
检查 alist 后台 > 任务 > 上传 中对应的上传任务的状态, 如果任务状态为成功但目录中不显示, 是由于 alist 缓存了目录结构, 参考文档可以调整缓存时间 alist 缓存了目录结构, 参考文档可以调整缓存时间
https://alist.nn.ci/zh/guide/drivers/common.html#缓存过期 https://alist.nn.ci/zh/guide/drivers/common.html#缓存过期

View File

@@ -4,9 +4,10 @@
Bot 接受两种消息: 文件和链接. Bot 接受两种消息: 文件和链接.
目前, 链接仅支持公开频道 (具有用户名) 的链接, 例如: `https://t.me/acherkrau/1097`. 支持以下链接:
**即使频道禁止了转发和保存, Bot 依然可以下载其文件.** 1. 公开频道 (具有用户名) 的消息链接, 例如: `https://t.me/acherkrau/1097`. **即使频道禁止了转发和保存, Bot 依然可以下载其文件.**
2. Telegra.ph 的文章链接, Bot 将下载其中的所有图片
## 静默模式 (silent) ## 静默模式 (silent)
@@ -32,4 +33,6 @@ Bot 接受两种消息: 文件和链接.
- 网络不稳定时, 任务失败率高. - 网络不稳定时, 任务失败率高.
- 无法在中间层对文件进行处理, 例如自动文件类型识别. - 无法在中间层对文件进行处理, 例如自动文件类型识别.
虽然目前 Bot 适配的所有存储端 (Alist, 本地磁盘, Webdav) 都支持 Stream 模式, 但今后可能会有不支持的存储端, 此时即使开启 Stream 模式, Bot 也会自动切换到普通模式. **不支持** Stream 模式的存储端:
- alist

View File

@@ -30,4 +30,6 @@ nav:
- index.md - index.md
- deploy.md - deploy.md
- help.md - help.md
- experimental.md
- faq.md - faq.md
- contribute.md

97
go.mod
View File

@@ -4,24 +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
golang.org/x/net v0.35.0 golang.org/x/net v0.40.0
golang.org/x/time v0.10.0 golang.org/x/time v0.11.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
@@ -31,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
) )

225
go.sum
View File

@@ -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,15 +220,23 @@ 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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -183,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=
@@ -255,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=

View File

@@ -7,13 +7,11 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path" "path"
"sync"
"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"
) )
@@ -43,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{
@@ -81,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
@@ -98,28 +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 {
logger.L.Infof("Saving file %s to %s", filePath, storagePath) common.Log.Infof("Saving file to %s", storagePath)
file, err := os.Open(filePath)
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 {
@@ -148,91 +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)
} }
type uploadStream struct {
ctx context.Context
client *http.Client
token string
storagePath string
baseURL string
pr *io.PipeReader
pw *io.PipeWriter
errChan chan error
once sync.Once
}
func (us *uploadStream) Write(p []byte) (int, error) {
return us.pw.Write(p)
}
func (us *uploadStream) Close() error {
var uploadErr error
us.once.Do(func() {
if err := us.pw.Close(); err != nil {
uploadErr = fmt.Errorf("failed to close pipe writer: %w", err)
return
}
if err := <-us.errChan; err != nil {
uploadErr = err
}
})
return uploadErr
}
func (a *Alist) NewUploadStream(ctx context.Context, storagePath string) (io.WriteCloser, error) {
if a.token == "" {
if err := a.getToken(); err != nil {
return nil, fmt.Errorf("not logged in to Alist: %w", err)
}
}
pr, pw := io.Pipe()
// 创建上传流对象
us := &uploadStream{
ctx: ctx,
client: a.client,
token: a.token,
storagePath: storagePath,
baseURL: a.baseURL,
pr: pr,
pw: pw,
errChan: make(chan error, 1),
}
go func() {
defer close(us.errChan)
req, err := http.NewRequestWithContext(ctx, http.MethodPut, a.baseURL+"/api/fs/put", pr)
if err != nil {
us.errChan <- fmt.Errorf("failed to create request: %w", err)
return
}
req.Header.Set("Authorization", a.token)
req.Header.Set("File-Path", url.PathEscape(storagePath))
req.Header.Set("As-Task", "true")
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := a.client.Do(req)
if err != nil {
us.errChan <- fmt.Errorf("failed to send request: %w", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
us.errChan <- fmt.Errorf("failed to upload file, status code: %d, response: %s", resp.StatusCode, string(body))
return
}
us.errChan <- nil
}()
return us, nil
}

View File

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

View File

@@ -8,8 +8,8 @@ import (
"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"
"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"
) )
@@ -41,8 +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 {
logger.L.Infof("Saving file %s to %s", filePath, storagePath) 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
@@ -50,24 +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)
}
func (l *Local) JoinStoragePath(task types.Task) string {
return filepath.Join(l.config.BasePath, task.StoragePath)
}
func (l *Local) NewUploadStream(ctx context.Context, path string) (io.WriteCloser, error) {
absPath, err := filepath.Abs(path)
if err != nil {
return nil, err
}
if err := fileutil.CreateDir(filepath.Dir(absPath)); err != nil {
return nil, err
}
file, err := os.Create(absPath) file, err := os.Create(absPath)
if err != nil { if err != nil {
return nil, err return err
} }
return file, nil defer file.Close()
_, err = io.Copy(file, r)
return err
} }

72
storage/minio/client.go Normal file
View 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
}

View File

@@ -5,25 +5,27 @@ import (
"fmt" "fmt"
"io" "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 StreamStorage interface { type StorageNotSupportStream interface {
Storage Storage
NewUploadStream(ctx context.Context, path string) (io.WriteCloser, error) NotSupportStream() string
} }
var Storages = make(map[string]Storage) var Storages = make(map[string]Storage)
@@ -90,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())
@@ -107,14 +110,14 @@ 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() { for user := range config.Cfg.GetUsersID() {
UserStorages[int64(user)] = GetUserStorages(int64(user)) UserStorages[int64(user)] = GetUserStorages(int64(user))
} }

View File

@@ -5,7 +5,11 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"path"
"strings" "strings"
"github.com/krau/SaveAny-Bot/types"
) )
type Client struct { type Client struct {
@@ -15,6 +19,14 @@ type Client struct {
httpClient *http.Client 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 { func NewClient(baseURL, username, password string, httpClient *http.Client) *Client {
if !strings.HasSuffix(baseURL, "/") { if !strings.HasSuffix(baseURL, "/") {
baseURL += "/" baseURL += "/"
@@ -30,34 +42,86 @@ func NewClient(baseURL, username, password string, httpClient *http.Client) *Cli
} }
} }
func (c *Client) doRequest(ctx context.Context, method, url string, body io.Reader) (*http.Response, error) { func (c *Client) doRequest(ctx context.Context, method WebdavMethod, url string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, url, body) req, err := http.NewRequestWithContext(ctx, string(method), url, body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if c.Username != "" && c.Password != "" { if c.Username != "" && c.Password != "" {
req.SetBasicAuth(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) return c.httpClient.Do(req)
} }
func (c *Client) MkDir(ctx context.Context, dirPath string) error { func (c *Client) Exists(ctx context.Context, remotePath string) (bool, error) {
url := c.BaseURL + dirPath url := c.BaseURL + remotePath
resp, err := c.doRequest(ctx, "MKCOL", url, nil) resp, err := c.doRequest(ctx, WebdavMethodPropfind, url, nil)
if err != nil { if err != nil {
return err return false, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 { 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 return nil
} }
return fmt.Errorf("MKCOL: %s", resp.Status) 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 { func (c *Client) WriteFile(ctx context.Context, remotePath string, content io.Reader) error {
url := c.BaseURL + remotePath u, err := url.Parse(c.BaseURL)
resp, err := c.doRequest(ctx, "PUT", url, content) 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 { if err != nil {
return err return err
} }
@@ -67,4 +131,5 @@ func (c *Client) WriteFile(ctx context.Context, remotePath string, content io.Re
return nil return nil
} }
return fmt.Errorf("PUT: %s", resp.Status) return fmt.Errorf("PUT: %s", resp.Status)
} }

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

View File

@@ -1,58 +0,0 @@
package webdav
import (
"context"
"fmt"
"io"
"path"
"github.com/krau/SaveAny-Bot/logger"
)
type WebdavWriter struct {
pipeWriter *io.PipeWriter
done chan error
path string
}
func (w *WebdavWriter) Write(p []byte) (n int, err error) {
return w.pipeWriter.Write(p)
}
func (w *WebdavWriter) Close() error {
if err := w.pipeWriter.Close(); err != nil {
return err
}
if err := <-w.done; err != nil {
return fmt.Errorf("upload failed: %w", err)
}
return nil
}
func (w *Webdav) NewUploadStream(ctx context.Context, storagePath string) (io.WriteCloser, error) {
if err := w.client.MkDir(ctx, path.Dir(storagePath)); err != nil {
logger.L.Errorf("Failed to create directory %s: %v", path.Dir(storagePath), err)
return nil, ErrFailedToCreateDirectory
}
pipeReader, pipeWriter := io.Pipe()
done := make(chan error, 1)
go func() {
defer func() {
if err := recover(); err != nil {
done <- fmt.Errorf("panic during upload: %v", err)
}
}()
err := w.client.WriteFile(ctx, storagePath, pipeReader)
pipeReader.Close()
done <- err
}()
return &WebdavWriter{
pipeWriter: pipeWriter,
done: done,
path: storagePath,
}, nil
}

View File

@@ -3,13 +3,13 @@ package webdav
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"net/http" "net/http"
"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"
) )
@@ -41,26 +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 {
logger.L.Infof("Saving file %s to %s", filePath, storagePath) return path.Join(w.config.BasePath, task.StoragePath)
}
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 { if err := w.client.MkDir(ctx, path.Dir(storagePath)); err != nil {
logger.L.Errorf("Failed to create directory %s: %v", path.Dir(storagePath), err) 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.WriteFile(ctx, storagePath, file); 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)
}

83
types/task.go Normal file
View File

@@ -0,0 +1,83 @@
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
FileDBID uint
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)
}

View File

@@ -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,67 +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
Cancel context.CancelFunc
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) Key() string { type RuleType string
return fmt.Sprintf("%d:%d", t.FileChatID, t.FileMessageID)
}
func (t Task) String() string { const (
return fmt.Sprintf("[%d:%d]:%s", t.FileChatID, t.FileMessageID, t.File.FileName) RuleTypeFileNameRegex RuleType = "FILENAME-REGEX"
} RuleTypeMessageRegex RuleType = "MESSAGE-REGEX"
)
func (t Task) FileName() string { var RuleTypes = []RuleType{RuleTypeFileNameRegex, RuleTypeMessageRegex}
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)
}

12
types/utils.go Normal file
View 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))
}