Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a746cc0fc7 | ||
|
|
0fb5634874 | ||
|
|
930e838b2e | ||
|
|
1701d1ab86 | ||
|
|
a17492d4ae | ||
|
|
a32bf43cdc | ||
|
|
a25a58f8a2 | ||
|
|
804a86cbdd | ||
|
|
cf1e9299c0 | ||
|
|
e3f7380341 | ||
|
|
6c6ee77067 | ||
|
|
f00aa189e3 | ||
|
|
48ceb87e29 | ||
|
|
cd663d05ca | ||
|
|
a778b0fa8c | ||
|
|
2a9c615819 | ||
|
|
2f60af858d | ||
|
|
741ceb87fb | ||
|
|
9795a85dd6 | ||
|
|
45974917f2 | ||
|
|
a146871a9d | ||
|
|
b76c1d5ccc | ||
|
|
79fba918cf | ||
|
|
e1bc80ab7d | ||
|
|
8d0851f37a | ||
|
|
4b5cabc76e | ||
|
|
3b42ba86b3 | ||
|
|
2b0bfac171 | ||
|
|
a76b289d17 | ||
|
|
4ac65622ee | ||
|
|
7bac97d533 | ||
|
|
169197c8fe | ||
|
|
a166ff1e64 | ||
|
|
f360052784 | ||
|
|
15f263cb50 | ||
|
|
420e0b1a4f | ||
|
|
71bd6a9480 | ||
|
|
5b9e177af4 | ||
|
|
9e226ae592 | ||
|
|
a91cce9680 | ||
|
|
1128ee2081 | ||
|
|
e3cd659eb3 | ||
|
|
454d69c9d4 | ||
|
|
71e5007228 | ||
|
|
3e5cd60cf7 | ||
|
|
0297fdce71 | ||
|
|
20e06fbf46 | ||
|
|
fbdfc04ad8 | ||
|
|
32bd391129 | ||
|
|
183edd0dbe | ||
|
|
75f6b444fc | ||
|
|
4294966ab7 | ||
|
|
edef778959 | ||
|
|
0bd9b77856 | ||
|
|
d5f214494c | ||
|
|
b5af39c132 | ||
|
|
4aba14c4c7 | ||
|
|
a66fa5df81 | ||
|
|
f58e28412f | ||
|
|
c8376f78a2 | ||
|
|
5d6b1003e9 | ||
|
|
5ef7c5ce60 | ||
|
|
03c833acbf | ||
|
|
0d369c7c79 | ||
|
|
7a288e3155 | ||
|
|
f9d51a688c | ||
|
|
dcc3f5f99b | ||
|
|
a04c46fab8 | ||
|
|
b3c59fde72 | ||
|
|
d1d284eedc |
60
.github/workflows/build-docker.yml
vendored
Normal file
60
.github/workflows/build-docker.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Build and Publish Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
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
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=sha
|
||||||
|
type=raw,value=latest
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.title=${{ env.IMAGE_NAME }}
|
||||||
|
org.opencontainers.image.source=https://github.com/krau/SaveAny-Bot
|
||||||
|
org.opencontainers.image.url=https://github.com/krau/SaveAny-Bot
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
|
|
||||||
- run: npx changelogithub
|
- run: npx changelogithub
|
||||||
env:
|
env:
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
- name: Release Go Binary
|
- name: Release Go Binary
|
||||||
uses: wangyoucao577/go-release-action@v1
|
uses: wangyoucao577/go-release-action@v1
|
||||||
with:
|
with:
|
||||||
pre_command: export
|
pre_command: export CGO_ENABLED=0
|
||||||
goos: ${{ matrix.goos }}
|
goos: ${{ matrix.goos }}
|
||||||
goarch: ${{ matrix.goarch }}
|
goarch: ${{ matrix.goarch }}
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,4 +3,6 @@ logs/
|
|||||||
tmp/
|
tmp/
|
||||||
data/
|
data/
|
||||||
downloads/
|
downloads/
|
||||||
cache/
|
cache/
|
||||||
|
session.*
|
||||||
|
cache.db
|
||||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM golang:alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.* ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o saveany-bot .
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/saveany-bot .
|
||||||
|
|
||||||
|
CMD ["./saveany-bot"]
|
||||||
143
LICENSE
143
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,17 +7,15 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -72,7 +60,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|||||||
77
README.md
77
README.md
@@ -1 +1,76 @@
|
|||||||
# Save Any Bot
|
<div align="center">
|
||||||
|
|
||||||
|
# <img src="docs/logo.jpg" width="45" align="center"> Save Any Bot
|
||||||
|
|
||||||
|
把 Telegram 的文件保存到各类存储端.
|
||||||
|
|
||||||
|
> _就像 PikPak Bot 一样_
|
||||||
|
|
||||||
|
</div
|
||||||
|
|
||||||
|
Demo Video:
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[SaveAny-Bot 演示视频 | The Demo of SaveAny-Bot.webm](https://github.com/user-attachments/assets/a0de2453-a4d1-4a12-81fb-9d84856dce09)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
|
||||||
|
|
||||||
|
在解压后目录新建 `config.toml` 文件, 参考 [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
||||||
|
|
||||||
|
运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x saveany-bot
|
||||||
|
./saveany-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加为 systemd 服务
|
||||||
|
|
||||||
|
创建文件 `/etc/systemd/system/saveany-bot.service` 并写入以下内容:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=SaveAnyBot
|
||||||
|
After=systemd-user-sessions.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/yourpath/
|
||||||
|
ExecStart=/yourpath/saveany-bot
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
设为开机启动并启动服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl enable --now saveany-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## 更新
|
||||||
|
|
||||||
|
使用 `upgrade` 或 `up` 升级到最新版
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./saveany-bot upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
向 Bot 发送(转发)文件, 按照提示操作.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
- [gotd](https://github.com/gotd/td)
|
||||||
|
- [TG-FileStreamBot](https://github.com/EverythingSuckz/TG-FileStreamBot)
|
||||||
|
- [gotgproto](https://github.com/celestix/gotgproto)
|
||||||
|
- All the dependencies
|
||||||
|
|||||||
160
bot/bot.go
160
bot/bot.go
@@ -1,110 +1,82 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/celestix/gotgproto"
|
||||||
|
"github.com/celestix/gotgproto/sessionMaker"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/gotd/td/telegram/dcs"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
"github.com/mymmrac/telego"
|
"golang.org/x/net/proxy"
|
||||||
"github.com/mymmrac/telego/telegohandler"
|
|
||||||
"github.com/mymmrac/telego/telegoutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var Client *gotgproto.Client
|
||||||
Bot *telego.Bot
|
|
||||||
)
|
func newProxyDialer(proxyUrl string) (proxy.Dialer, error) {
|
||||||
|
url, err := url.Parse(proxyUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return proxy.FromURL(url, proxy.Direct)
|
||||||
|
}
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
logger.L.Debug("Initializing bot...")
|
logger.L.Info("Initializing client...")
|
||||||
var err error
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
Bot, err = telego.NewBot(
|
defer cancel()
|
||||||
config.Cfg.Telegram.Token,
|
resultChan := make(chan struct {
|
||||||
telego.WithDefaultLogger(false, true),
|
client *gotgproto.Client
|
||||||
telego.WithAPIServer(config.Cfg.Telegram.API),
|
err error
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Fatal("Failed to create bot: ", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
Bot.SetMyCommands(&telego.SetMyCommandsParams{
|
|
||||||
Commands: []telego.BotCommand{
|
|
||||||
{Command: "start", Description: "开始使用"},
|
|
||||||
{Command: "help", Description: "显示帮助"},
|
|
||||||
{Command: "silent", Description: "静默模式"},
|
|
||||||
{Command: "storage", Description: "设置默认存储位置"},
|
|
||||||
{Command: "save", Description: "保存文件"},
|
|
||||||
{Command: "clean", Description: "清除文件记录"},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
logger.L.Debug("Bot initialized")
|
go func() {
|
||||||
}
|
var resolver dcs.Resolver
|
||||||
|
if config.Cfg.Telegram.Proxy.Enable && config.Cfg.Telegram.Proxy.URL != "" {
|
||||||
func Run() {
|
dialer, err := newProxyDialer(config.Cfg.Telegram.Proxy.URL)
|
||||||
if Bot == nil {
|
if err != nil {
|
||||||
Init()
|
resultChan <- struct {
|
||||||
}
|
client *gotgproto.Client
|
||||||
logger.L.Info("Start polling...")
|
err error
|
||||||
updates, err := Bot.UpdatesViaLongPolling(&telego.GetUpdatesParams{
|
}{nil, err}
|
||||||
Offset: -1,
|
return
|
||||||
AllowedUpdates: []string{
|
}
|
||||||
telego.MessageUpdates,
|
resolver = dcs.Plain(dcs.PlainOptions{
|
||||||
telego.CallbackQueryUpdates,
|
Dial: dialer.(proxy.ContextDialer).DialContext,
|
||||||
},
|
})
|
||||||
})
|
} else {
|
||||||
if err != nil {
|
resolver = dcs.DefaultResolver()
|
||||||
logger.L.Fatal("Failed to start polling: ", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
botHandler, err := telegohandler.NewBotHandler(Bot, updates)
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Fatal("Failed to create bot handler: ", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer botHandler.Stop()
|
|
||||||
defer Bot.StopLongPolling()
|
|
||||||
|
|
||||||
botHandler.Use(telegohandler.PanicRecovery())
|
|
||||||
baseGroup := botHandler.BaseGroup()
|
|
||||||
|
|
||||||
registerHandlers(baseGroup)
|
|
||||||
|
|
||||||
botHandler.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerHandlers(hg *telegohandler.HandlerGroup) {
|
|
||||||
msgGroup := hg.Group(telegohandler.AnyMessage())
|
|
||||||
msgGroup.Use(func(bot *telego.Bot, update telego.Update, next telegohandler.Handler) {
|
|
||||||
if !slice.Contain(config.Cfg.Telegram.Admins, update.Message.From.ID) {
|
|
||||||
bot.SendMessage(telegoutil.Message(update.Message.Chat.ChatID(), "抱歉, 该 Bot 为个人使用设计, 您可以部署自己的 SaveAnyBot 实例: https://github.com/krau/SaveAny-Bot"))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
next(bot, update)
|
client, err := gotgproto.NewClient(config.Cfg.Telegram.AppID,
|
||||||
})
|
config.Cfg.Telegram.AppHash,
|
||||||
|
gotgproto.ClientTypeBot(config.Cfg.Telegram.Token),
|
||||||
|
&gotgproto.ClientOpts{
|
||||||
|
Session: sessionMaker.SqlSession(sqlite.Open("data/session.db")),
|
||||||
|
DisableCopyright: true,
|
||||||
|
Middlewares: FloodWaitMiddleware(),
|
||||||
|
Resolver: resolver,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resultChan <- struct {
|
||||||
|
client *gotgproto.Client
|
||||||
|
err error
|
||||||
|
}{client, err}
|
||||||
|
}()
|
||||||
|
|
||||||
msgGroup.HandleMessageCtx(Start, telegohandler.CommandEqual("start"))
|
select {
|
||||||
msgGroup.HandleMessageCtx(Help, telegohandler.CommandEqual("help"))
|
case <-ctx.Done():
|
||||||
msgGroup.HandleMessageCtx(ChangeSilentMode, telegohandler.CommandEqual("silent"))
|
logger.L.Fatal("Failed to initialize client: timeout")
|
||||||
msgGroup.HandleMessageCtx(SetDefaultStorage, telegohandler.CommandEqual("storage"))
|
os.Exit(1)
|
||||||
msgGroup.HandleMessageCtx(SaveFile, telegohandler.CommandEqual("save"))
|
case result := <-resultChan:
|
||||||
msgGroup.HandleMessageCtx(CleanReceivedFile, telegohandler.CommandEqual("clean"))
|
if result.err != nil {
|
||||||
|
logger.L.Fatalf("Failed to initialize client: %s", result.err)
|
||||||
msgGroup.HandleMessageCtx(HandleFileMessage, func(update telego.Update) bool {
|
os.Exit(1)
|
||||||
return update.Message.Document != nil || update.Message.Video != nil || update.Message.Audio != nil
|
|
||||||
})
|
|
||||||
|
|
||||||
callbackGroup := hg.Group(telegohandler.AnyCallbackQueryWithMessage())
|
|
||||||
callbackGroup.Use(func(bot *telego.Bot, update telego.Update, next telegohandler.Handler) {
|
|
||||||
if !slice.Contain(config.Cfg.Telegram.Admins, update.CallbackQuery.From.ID) {
|
|
||||||
bot.AnswerCallbackQuery(telegoutil.
|
|
||||||
CallbackQuery(update.CallbackQuery.ID).
|
|
||||||
WithText("抱歉, 该 Bot 为个人使用设计, 您可以部署自己的 SaveAnyBot 实例: https://github.com/krau/SaveAny-Bot").
|
|
||||||
WithShowAlert().
|
|
||||||
WithCacheTime(60))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
next(bot, update)
|
Client = result.client
|
||||||
})
|
RegisterHandlers(Client.Dispatcher)
|
||||||
|
logger.L.Info("Client initialized")
|
||||||
callbackGroup.HandleCallbackQueryCtx(AddToQueue, telegohandler.CallbackDataPrefix("add"))
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
502
bot/handlers.go
502
bot/handlers.go
@@ -1,260 +1,396 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/gookit/goutil/maputil"
|
"github.com/gookit/goutil/maputil"
|
||||||
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
|
"github.com/celestix/gotgproto/dispatcher/handlers"
|
||||||
|
"github.com/celestix/gotgproto/dispatcher/handlers/filters"
|
||||||
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"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/logger"
|
||||||
"github.com/krau/SaveAny-Bot/model"
|
|
||||||
"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"
|
||||||
"github.com/mymmrac/telego"
|
|
||||||
"github.com/mymmrac/telego/telegoutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Start(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
|
||||||
if err := dao.CreateUser(message.From.ID); err != nil {
|
dispatcher.AddHandler(handlers.NewMessage(filters.Message.All, checkPermission))
|
||||||
logger.L.Errorf("Failed to create user: %s", err)
|
dispatcher.AddHandler(handlers.NewCommand("start", start))
|
||||||
return
|
dispatcher.AddHandler(handlers.NewCommand("help", help))
|
||||||
}
|
dispatcher.AddHandler(handlers.NewCommand("silent", silent))
|
||||||
Help(ctx, bot, message)
|
dispatcher.AddHandler(handlers.NewCommand("storage", setDefaultStorage))
|
||||||
|
dispatcher.AddHandler(handlers.NewCommand("save", saveCmd))
|
||||||
|
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("add"), AddToQueue))
|
||||||
|
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Help(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
const noPermissionText string = `
|
||||||
helpText := `
|
本 Bot 仅限个人使用.
|
||||||
|
您可以部署自己的实例: https://github.com/krau/SaveAny-Bot
|
||||||
|
`
|
||||||
|
|
||||||
|
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
userID := update.GetUserChat().GetID()
|
||||||
|
if !slice.Contain(config.Cfg.Telegram.Admins, userID) {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
return dispatcher.ContinueGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func start(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
if err := dao.CreateUser(update.GetUserChat().GetID()); err != nil {
|
||||||
|
logger.L.Errorf("Failed to create user: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
return help(ctx, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
const helpText string = `
|
||||||
SaveAny Bot - 转存你的 Telegram 文件
|
SaveAny Bot - 转存你的 Telegram 文件
|
||||||
命令:
|
命令:
|
||||||
/start - 开始使用
|
/start - 开始使用
|
||||||
/help - 显示帮助
|
/help - 显示帮助
|
||||||
/silent - 静默模式
|
/silent - 静默模式
|
||||||
/storage - 设置默认存储位置
|
/storage - 设置默认存储位置
|
||||||
/save - 保存文件
|
/save [自定义文件名] - 保存文件
|
||||||
/clean - 清除文件记录
|
|
||||||
|
|
||||||
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
|
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
|
||||||
`
|
`
|
||||||
ReplyMessage(message, helpText)
|
|
||||||
|
func help(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(helpText), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChangeSilentMode(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
func silent(ctx *ext.Context, update *ext.Update) error {
|
||||||
user, err := dao.GetUserByUserID(message.From.ID)
|
user, err := dao.GetUserByUserID(update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Error(err)
|
logger.L.Errorf("Failed to get user: %s", err)
|
||||||
return
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
user.Silent = !user.Silent
|
user.Silent = !user.Silent
|
||||||
err = dao.UpdateUser(user)
|
if err := dao.UpdateUser(user); err != nil {
|
||||||
if err != nil {
|
logger.L.Errorf("Failed to update user: %s", err)
|
||||||
logger.L.Error(err)
|
return dispatcher.EndGroups
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ReplyMessage(message, fmt.Sprintf("已%s静默模式", map[bool]string{true: "开启", false: "关闭"}[user.Silent]))
|
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已%s静默模式", func() string {
|
||||||
|
if user.Silent {
|
||||||
|
return "开启"
|
||||||
|
}
|
||||||
|
return "关闭"
|
||||||
|
}())), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetDefaultStorage(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
func setDefaultStorage(ctx *ext.Context, update *ext.Update) error {
|
||||||
if len(storage.Storages) == 0 {
|
if len(storage.Storages) == 0 {
|
||||||
ReplyMessage(message, "当前无可用存储端, 请检查配置.")
|
ctx.Reply(update, ext.ReplyTextString("未配置存储"), nil)
|
||||||
return
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
_, _, args := telegoutil.ParseCommand(message.Text)
|
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||||
availableStorages := maputil.Keys(storage.Storages)
|
avaliableStorages := maputil.Keys(storage.Storages)
|
||||||
if len(args) == 0 {
|
if len(args) < 2 {
|
||||||
text := EscapeMarkdown("请提供存储位置名称, 可用项:")
|
text := []styling.StyledTextOption{
|
||||||
for _, name := range availableStorages {
|
styling.Plain("请提供存储位置名称, 可用项:"),
|
||||||
text += fmt.Sprintf("\n`%s`", name)
|
|
||||||
}
|
}
|
||||||
text += fmt.Sprintf("\n`all`")
|
for _, name := range avaliableStorages {
|
||||||
bot.SendMessage(telegoutil.Message(message.Chat.ChatID(), text).WithParseMode(telego.ModeMarkdownV2))
|
text = append(text, styling.Plain("\n"))
|
||||||
return
|
text = append(text, styling.Code(name))
|
||||||
|
}
|
||||||
|
text = append(text, styling.Plain("\n示例: /storage local"))
|
||||||
|
ctx.Reply(update, ext.ReplyTextStyledTextArray(text), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
storageName := args[0]
|
storageName := args[1]
|
||||||
if !slice.Contain(availableStorages, storageName) {
|
if !slice.Contain(avaliableStorages, storageName) {
|
||||||
ReplyMessage(message, "参数错误")
|
ctx.Reply(update, ext.ReplyTextString("存储位置不存在"), nil)
|
||||||
return
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
user, err := dao.GetUserByUserID(message.From.ID)
|
user, err := dao.GetUserByUserID(update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Error(err)
|
logger.L.Errorf("Failed to get user: %s", err)
|
||||||
return
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
user.DefaultStorage = storageName
|
user.DefaultStorage = storageName
|
||||||
err = dao.UpdateUser(user)
|
if err := dao.UpdateUser(user); err != nil {
|
||||||
if err != nil {
|
logger.L.Errorf("Failed to update user: %s", err)
|
||||||
logger.L.Error(err)
|
return dispatcher.EndGroups
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ReplyMessage(message, fmt.Sprintf("已设置默认存储位置为: %s", storageName))
|
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已设置默认存储位置为 %s", storageName)), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveFile(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
targetMessage := message.ReplyToMessage
|
res, ok := update.EffectiveMessage.GetReplyTo()
|
||||||
if targetMessage == nil {
|
if !ok || res == nil {
|
||||||
ReplyMessage(message, "请回复要保存的文件")
|
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
||||||
return
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if targetMessage.Document == nil && targetMessage.Video == nil && targetMessage.Audio == nil {
|
replyHeader, ok := res.(*tg.MessageReplyHeader)
|
||||||
ReplyMessage(message, "回复的消息不包含文件")
|
if !ok {
|
||||||
return
|
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, "force", true)
|
replyToMsgID, ok := replyHeader.GetReplyToMsgID()
|
||||||
HandleFileMessage(ctx, bot, *targetMessage)
|
if !ok {
|
||||||
}
|
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
func CleanReceivedFile(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
|
||||||
targetMessage := message.ReplyToMessage
|
|
||||||
if targetMessage == nil {
|
|
||||||
ReplyMessage(message, "请回复要清除记录的文件")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if targetMessage.Document == nil && targetMessage.Video == nil && targetMessage.Audio == nil {
|
|
||||||
ReplyMessage(message, "回复的消息不包含文件")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var fileUniqueID string
|
|
||||||
if targetMessage.Document != nil {
|
|
||||||
fileUniqueID = targetMessage.Document.FileUniqueID
|
|
||||||
} else if targetMessage.Video != nil {
|
|
||||||
fileUniqueID = targetMessage.Video.FileUniqueID
|
|
||||||
} else if targetMessage.Audio != nil {
|
|
||||||
fileUniqueID = targetMessage.Audio.FileUniqueID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileUniqueID == "" {
|
msg, err := GetTGMessage(ctx, Client, replyToMsgID)
|
||||||
ReplyMessage(message, "文件信息获取失败")
|
|
||||||
return
|
supported, _ := supportedMediaFilter(msg)
|
||||||
|
if !supported {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("不支持的消息类型或消息中没有文件"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dao.DeleteReceivedFileByFileUniqueID(fileUniqueID); err != nil {
|
user, err := dao.GetUserByUserID(update.GetUserChat().GetID())
|
||||||
logger.L.Error(err)
|
|
||||||
ReplyMessage(message, "删除记录失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ReplyMessage(message, "记录已删除")
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleFileMessage(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
|
||||||
var fileID, fileName string
|
|
||||||
if message.Document != nil {
|
|
||||||
fileID = message.Document.FileID
|
|
||||||
fileName = message.Document.FileName
|
|
||||||
} else if message.Video != nil {
|
|
||||||
fileID = message.Video.FileID
|
|
||||||
fileName = message.Video.FileName
|
|
||||||
} else if message.Audio != nil {
|
|
||||||
fileID = message.Audio.FileID
|
|
||||||
fileName = message.Audio.FileName
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileID == "" || fileName == "" {
|
|
||||||
ReplyMessage(message, "文件信息获取失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user, err := dao.GetUserByUserID(message.From.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Error(err)
|
logger.L.Errorf("Failed to get user: %s", err)
|
||||||
return
|
return dispatcher.EndGroups
|
||||||
}
|
|
||||||
msg, err := ReplyMessage(message, "正在获取文件信息")
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
file, err := bot.GetFile(&telego.GetFileParams{FileID: fileID})
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Error(err)
|
|
||||||
ReplyMessage(message, "获取文件信息失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ctx.Value("force") == nil {
|
|
||||||
receivedFile, _ := dao.GetReceivedFileByFileID(file.FileID)
|
|
||||||
if receivedFile != nil && receivedFile.Processing {
|
|
||||||
bot.EditMessageText(&telego.EditMessageTextParams{
|
|
||||||
ChatID: message.Chat.ChatID(),
|
|
||||||
MessageID: msg.MessageID,
|
|
||||||
Text: "该文件或许正在处理中, 使用 /save 命令回复此文件以强制加入队列\n使用 /clean 命令回复此文件以清除对应的记录",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dao.AddReceivedFile(&model.ReceivedFile{
|
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
||||||
FileUniqueID: file.FileUniqueID,
|
if err != nil {
|
||||||
FileID: file.FileID,
|
logger.L.Errorf("Failed to reply: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdText := update.EffectiveMessage.Text
|
||||||
|
customFileName := strings.TrimSpace(strings.TrimPrefix(cmdText, "/save"))
|
||||||
|
|
||||||
|
file, err := FileFromMessage(ctx, Client, update.EffectiveChat().GetID(), msg.ID, customFileName)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to get file from message: %s", err)
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: "无法获取文件",
|
||||||
|
ID: replied.ID,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.FileName == "" {
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: "无法获取文件名",
|
||||||
|
ID: replied.ID,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedFile := &types.ReceivedFile{
|
||||||
Processing: false,
|
Processing: false,
|
||||||
FileName: fileName,
|
FileName: file.FileName,
|
||||||
FilePath: file.FilePath,
|
ChatID: update.EffectiveChat().GetID(),
|
||||||
FileSize: file.FileSize,
|
MessageID: replyToMsgID,
|
||||||
MediaGroupID: message.MediaGroupID,
|
ReplyMessageID: replied.ID,
|
||||||
ChatID: message.Chat.ChatID().ID,
|
}
|
||||||
MessageID: message.MessageID,
|
|
||||||
ReplyMessageID: msg.MessageID,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
||||||
logger.L.Error(err)
|
logger.L.Errorf("Failed to save received file: %s", err)
|
||||||
ReplyMessage(message, "创建任务失败")
|
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
return
|
Message: fmt.Sprintf("Failed to save received file: %s", err),
|
||||||
|
ID: replied.ID,
|
||||||
|
}); err != nil {
|
||||||
|
logger.L.Errorf("Failed to edit message: %s", err)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
if !user.Silent {
|
if !user.Silent {
|
||||||
bot.EditMessageText(&telego.EditMessageTextParams{
|
text := "请选择存储位置"
|
||||||
ChatID: message.Chat.ChatID(),
|
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
MessageID: msg.MessageID,
|
Message: text,
|
||||||
Text: "选择要保存的位置",
|
ReplyMarkup: getAddTaskMarkup(msg.ID),
|
||||||
ReplyMarkup: AddTaskReplyMarkup(message.MessageID),
|
ID: replied.ID,
|
||||||
})
|
})
|
||||||
return
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to reply: %s", err)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.DefaultStorage == "" {
|
if user.DefaultStorage == "" {
|
||||||
bot.EditMessageText(&telego.EditMessageTextParams{
|
ctx.Reply(update, ext.ReplyTextString("请先使用 /storage 设置默认存储位置"), nil)
|
||||||
ChatID: message.Chat.ChatID(),
|
return dispatcher.EndGroups
|
||||||
MessageID: msg.MessageID,
|
|
||||||
Text: "请先使用 /storage 命令设置默认存储位置, 或者关闭静默模式",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.AddTask(types.Task{
|
queue.AddTask(types.Task{
|
||||||
Ctx: context.TODO(),
|
Ctx: ctx,
|
||||||
FileID: file.FileID,
|
|
||||||
Status: types.Pending,
|
Status: types.Pending,
|
||||||
FileName: fileName,
|
File: file,
|
||||||
FilePath: file.FilePath,
|
|
||||||
Storage: types.StorageType(user.DefaultStorage),
|
Storage: types.StorageType(user.DefaultStorage),
|
||||||
ChatID: message.Chat.ChatID().ID,
|
ChatID: update.EffectiveChat().GetID(),
|
||||||
ReplyMessageID: msg.MessageID,
|
ReplyMessageID: replied.ID,
|
||||||
|
MessageID: msg.ID,
|
||||||
})
|
})
|
||||||
|
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", file.FileName, queue.Len()),
|
||||||
|
ID: replied.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to edit message: %s", err)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddToQueue(ctx context.Context, bot *telego.Bot, query telego.CallbackQuery) {
|
func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
|
||||||
args := strings.Split(query.Data, " ")
|
logger.L.Trace("Got media: ", update.EffectiveMessage.Media.TypeName())
|
||||||
messageID, _ := strconv.Atoi(args[1])
|
supported, err := supportedMediaFilter(update.EffectiveMessage.Message)
|
||||||
receivedFile, err := dao.GetReceivedFileByChatAndMessageID(query.Message.GetChat().ID, messageID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Error(err)
|
return err
|
||||||
bot.AnswerCallbackQuery(telegoutil.CallbackQuery(query.ID).WithShowAlert().WithText("获取文件信息失败").WithCacheTime(5))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if !supported {
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := dao.GetUserByUserID(update.GetUserChat().GetID())
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to get user: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to reply: %s", err)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
media := update.EffectiveMessage.Media
|
||||||
|
file, err := FileFromMedia(media, "")
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to get file from media: %s", err)
|
||||||
|
if errors.Is(err, ErrEmptyFileName) {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无法获取文件名, 请使用 /save <自定义文件名> 回复此文件"), nil)
|
||||||
|
} else {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if file.FileName == "" {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString("无法获取文件名"), nil)
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dao.SaveReceivedFile(&types.ReceivedFile{
|
||||||
|
Processing: false,
|
||||||
|
FileName: file.FileName,
|
||||||
|
ChatID: update.EffectiveChat().GetID(),
|
||||||
|
MessageID: update.EffectiveMessage.ID,
|
||||||
|
ReplyMessageID: msg.ID,
|
||||||
|
}); err != nil {
|
||||||
|
logger.L.Errorf("Failed to add received file: %s", err)
|
||||||
|
if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("Failed to add received file: %s", err),
|
||||||
|
ID: msg.ID,
|
||||||
|
}); err != nil {
|
||||||
|
logger.L.Errorf("Failed to edit message: %s", err)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.Silent {
|
||||||
|
text := "请选择存储位置"
|
||||||
|
_, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: text,
|
||||||
|
ReplyMarkup: getAddTaskMarkup(update.EffectiveMessage.ID),
|
||||||
|
ID: msg.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to edit message: %s", err)
|
||||||
|
}
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.DefaultStorage == "" {
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: "请先使用 /storage 设置默认存储位置",
|
||||||
|
ID: msg.ID,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
queue.AddTask(types.Task{
|
queue.AddTask(types.Task{
|
||||||
Ctx: context.TODO(),
|
Ctx: ctx,
|
||||||
FileID: receivedFile.FileID,
|
|
||||||
Status: types.Pending,
|
Status: types.Pending,
|
||||||
FileName: receivedFile.FileName,
|
File: file,
|
||||||
FilePath: receivedFile.FilePath,
|
Storage: types.StorageType(user.DefaultStorage),
|
||||||
Storage: types.StorageType(args[2]),
|
ChatID: update.EffectiveChat().GetID(),
|
||||||
ChatID: receivedFile.ChatID,
|
ReplyMessageID: msg.ID,
|
||||||
ReplyMessageID: receivedFile.ReplyMessageID,
|
MessageID: update.EffectiveMessage.ID,
|
||||||
})
|
})
|
||||||
bot.EditMessageText(&telego.EditMessageTextParams{
|
|
||||||
Text: fmt.Sprintf("已添加到队列, 当前排队中的任务数: %d", queue.Len()),
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
MessageID: query.Message.GetMessageID(),
|
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", file.FileName, queue.Len()),
|
||||||
ChatID: telegoutil.ID(receivedFile.ChatID),
|
ID: msg.ID,
|
||||||
})
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddToQueue(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
if !slice.Contain(config.Cfg.Telegram.Admins, update.CallbackQuery.UserID) {
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "你没有权限",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
args := strings.Split(string(update.CallbackQuery.Data), " ")
|
||||||
|
messageID, _ := strconv.Atoi(args[1])
|
||||||
|
logger.L.Tracef("Got add to queue: chatID: %d, messageID: %d, storage: %s", update.EffectiveChat().GetID(), messageID, args[2])
|
||||||
|
record, err := dao.GetReceivedFileByChatAndMessageID(update.EffectiveChat().GetID(), messageID)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to get received file: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: "查询记录失败",
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
if update.CallbackQuery.MsgID != record.ReplyMessageID {
|
||||||
|
record.ReplyMessageID = update.CallbackQuery.MsgID
|
||||||
|
if err := dao.SaveReceivedFile(record); err != nil {
|
||||||
|
logger.L.Errorf("Failed to update received file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := FileFromMessage(ctx, Client, record.ChatID, record.MessageID, record.FileName)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to get file from message: %s", err)
|
||||||
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
|
QueryID: update.CallbackQuery.QueryID,
|
||||||
|
Alert: true,
|
||||||
|
Message: fmt.Sprintf("获取消息中的文件失败: %s", err),
|
||||||
|
CacheTime: 5,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.AddTask(types.Task{
|
||||||
|
Ctx: ctx,
|
||||||
|
Status: types.Pending,
|
||||||
|
File: file,
|
||||||
|
Storage: types.StorageType(args[2]),
|
||||||
|
ChatID: record.ChatID,
|
||||||
|
ReplyMessageID: record.ReplyMessageID,
|
||||||
|
MessageID: record.MessageID,
|
||||||
|
})
|
||||||
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", record.FileName, queue.Len()),
|
||||||
|
ID: record.ReplyMessageID,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package bot
|
|
||||||
19
bot/middlewares.go
Normal file
19
bot/middlewares.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotd/contrib/middleware/floodwait"
|
||||||
|
"github.com/gotd/contrib/middleware/ratelimit"
|
||||||
|
"github.com/gotd/td/telegram"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FloodWaitMiddleware() []telegram.Middleware {
|
||||||
|
waiter := floodwait.NewSimpleWaiter().WithMaxRetries(5)
|
||||||
|
ratelimiter := ratelimit.New(rate.Every(time.Millisecond*100), 5)
|
||||||
|
return []telegram.Middleware{
|
||||||
|
waiter,
|
||||||
|
ratelimiter,
|
||||||
|
}
|
||||||
|
}
|
||||||
199
bot/utils.go
199
bot/utils.go
@@ -1,49 +1,186 @@
|
|||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"time"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/celestix/gotgproto"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/mymmrac/telego/telegoutil"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FileDownloadURL(filePath string) string {
|
var (
|
||||||
return Bot.FileDownloadURL(filePath)
|
ErrEmptyFileName = errors.New("file name is empty")
|
||||||
|
ErrEmptyDocument = errors.New("document is empty")
|
||||||
|
ErrEmptyPhoto = errors.New("photo is empty")
|
||||||
|
ErrEmptyPhotoSize = errors.New("photo size is empty")
|
||||||
|
ErrEmptyPhotoSizes = errors.New("photo size slice is empty")
|
||||||
|
)
|
||||||
|
|
||||||
|
func supportedMediaFilter(m *tg.Message) (bool, error) {
|
||||||
|
if not := m.Media == nil; not {
|
||||||
|
return false, dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
switch m.Media.(type) {
|
||||||
|
case *tg.MessageMediaDocument:
|
||||||
|
return true, nil
|
||||||
|
case *tg.MessageMediaPhoto:
|
||||||
|
return true, nil
|
||||||
|
default:
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReplyMessage(replyTo telego.Message, format string, args ...any) (*telego.Message, error) {
|
var StorageDisplayNames = map[string]string{
|
||||||
return Bot.SendMessage(telegoutil.Messagef(replyTo.Chat.ChatID(), format, args...).
|
"all": "全部",
|
||||||
WithReplyParameters(&telego.ReplyParameters{
|
"local": "服务器磁盘",
|
||||||
MessageID: replyTo.MessageID,
|
"alist": "Alist",
|
||||||
}))
|
"webdav": "WebDAV",
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddTaskReplyMarkup(messageID int) *telego.InlineKeyboardMarkup {
|
func getAddTaskMarkup(messageID int) *tg.ReplyInlineMarkup {
|
||||||
storageButtons := make([]telego.InlineKeyboardButton, 0)
|
storageButtons := make([]tg.KeyboardButtonClass, 0)
|
||||||
if config.Cfg.Storage.Local.Enable {
|
for _, name := range storage.StorageKeys {
|
||||||
storageButtons = append(storageButtons, telegoutil.InlineKeyboardButton("服务器磁盘").
|
storageButtons = append(storageButtons, &tg.KeyboardButtonCallback{
|
||||||
WithCallbackData(fmt.Sprintf("add %d local", messageID)))
|
Text: StorageDisplayNames[string(name)],
|
||||||
}
|
Data: []byte(fmt.Sprintf("add %d %s", messageID, name)),
|
||||||
if config.Cfg.Storage.Alist.Enable {
|
|
||||||
storageButtons = append(storageButtons, telegoutil.InlineKeyboardButton("Alist").
|
|
||||||
WithCallbackData(fmt.Sprintf("add %d alist", messageID)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(storageButtons) > 1 {
|
|
||||||
return telegoutil.InlineKeyboard(storageButtons, []telego.InlineKeyboardButton{
|
|
||||||
telegoutil.InlineKeyboardButton("全部").WithCallbackData(fmt.Sprintf("add %d all", messageID)),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(storageButtons) == 1 {
|
|
||||||
return telegoutil.InlineKeyboard(storageButtons)
|
if len(storageButtons) < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(storageButtons) == 1 {
|
||||||
|
return &tg.ReplyInlineMarkup{
|
||||||
|
Rows: []tg.KeyboardButtonRow{
|
||||||
|
{
|
||||||
|
Buttons: storageButtons,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &tg.ReplyInlineMarkup{
|
||||||
|
Rows: []tg.KeyboardButtonRow{
|
||||||
|
{
|
||||||
|
Buttons: storageButtons,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Buttons: []tg.KeyboardButtonClass{
|
||||||
|
&tg.KeyboardButtonCallback{
|
||||||
|
Text: "全部",
|
||||||
|
Data: []byte(fmt.Sprintf("add %d all", messageID)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var markdownRe = regexp.MustCompile("([" + regexp.QuoteMeta(`\_*[]()~`+"`"+`>#+-=|{}.!`) + "])")
|
func FileFromMedia(media tg.MessageMediaClass, customFileName string) (*types.File, error) {
|
||||||
|
switch media := media.(type) {
|
||||||
|
case *tg.MessageMediaDocument:
|
||||||
|
document, ok := media.Document.AsNotEmpty()
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrEmptyDocument
|
||||||
|
}
|
||||||
|
if customFileName != "" {
|
||||||
|
return &types.File{
|
||||||
|
Location: document.AsInputDocumentFileLocation(),
|
||||||
|
FileSize: document.Size,
|
||||||
|
FileName: customFileName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
fileName := ""
|
||||||
|
for _, attribute := range document.Attributes {
|
||||||
|
if name, ok := attribute.(*tg.DocumentAttributeFilename); ok {
|
||||||
|
fileName = name.GetFileName()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fileName == "" {
|
||||||
|
return nil, ErrEmptyFileName
|
||||||
|
}
|
||||||
|
return &types.File{
|
||||||
|
Location: document.AsInputDocumentFileLocation(),
|
||||||
|
FileSize: document.Size,
|
||||||
|
FileName: fileName,
|
||||||
|
}, nil
|
||||||
|
case *tg.MessageMediaPhoto:
|
||||||
|
photo, ok := media.Photo.AsNotEmpty()
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrEmptyPhoto
|
||||||
|
}
|
||||||
|
sizes := photo.Sizes
|
||||||
|
if len(sizes) == 0 {
|
||||||
|
return nil, ErrEmptyPhotoSizes
|
||||||
|
}
|
||||||
|
photoSize := sizes[len(sizes)-1]
|
||||||
|
size, ok := photoSize.AsNotEmpty()
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrEmptyPhotoSize
|
||||||
|
}
|
||||||
|
location := new(tg.InputPhotoFileLocation)
|
||||||
|
location.ID = photo.GetID()
|
||||||
|
location.AccessHash = photo.GetAccessHash()
|
||||||
|
location.FileReference = photo.GetFileReference()
|
||||||
|
location.ThumbSize = size.GetType()
|
||||||
|
fileName := customFileName
|
||||||
|
if fileName == "" {
|
||||||
|
fileName = fmt.Sprintf("photo_%s_%d.jpg", time.Now().Format("2006-01-02_15-04-05"), photo.GetID())
|
||||||
|
}
|
||||||
|
return &types.File{
|
||||||
|
Location: location,
|
||||||
|
FileSize: 0,
|
||||||
|
FileName: fileName,
|
||||||
|
}, nil
|
||||||
|
|
||||||
func EscapeMarkdown(text string) string {
|
}
|
||||||
return markdownRe.ReplaceAllString(text, "\\$1")
|
return nil, fmt.Errorf("unexpected type %T", media)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileFromMessage(ctx context.Context, client *gotgproto.Client, chatID int64, messageID int, customFileName string) (*types.File, error) {
|
||||||
|
key := fmt.Sprintf("file:%d:%d", chatID, messageID)
|
||||||
|
logger.L.Debugf("Getting file: %s", key)
|
||||||
|
var cachedFile types.File
|
||||||
|
err := common.Cache.Get(key, &cachedFile)
|
||||||
|
if err == nil {
|
||||||
|
return &cachedFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
message, err := GetTGMessage(ctx, client, messageID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
file, err := FileFromMedia(message.Media, customFileName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := common.Cache.Set(key, file, 3600); err != nil {
|
||||||
|
logger.L.Errorf("Failed to cache file: %s", err)
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTGMessage(ctx context.Context, client *gotgproto.Client, messageID int) (*tg.Message, error) {
|
||||||
|
logger.L.Debugf("Fetching message: %d", messageID)
|
||||||
|
res, err := client.API().MessagesGetMessages(ctx, []tg.InputMessageClass{
|
||||||
|
&tg.InputMessageID{
|
||||||
|
ID: messageID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
messages := res.(*tg.MessagesMessages)
|
||||||
|
msg := messages.Messages[0]
|
||||||
|
if _, ok := msg.(*tg.Message); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected type %T, this file may be deleted", msg)
|
||||||
|
}
|
||||||
|
return msg.(*tg.Message), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import (
|
|||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "saveany-bot",
|
Use: "saveany-bot",
|
||||||
Short: "saveany-bot",
|
Short: "saveany-bot",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: Run,
|
||||||
Run()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
|
|||||||
46
cmd/run.go
46
cmd/run.go
@@ -1,13 +1,51 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/bootstrap"
|
"github.com/krau/SaveAny-Bot/bootstrap"
|
||||||
"github.com/krau/SaveAny-Bot/bot"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/core"
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run() {
|
func Run(_ *cobra.Command, _ []string) {
|
||||||
bootstrap.InitAll()
|
bootstrap.InitAll()
|
||||||
go core.Run()
|
core.Run()
|
||||||
bot.Run()
|
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
sig := <-quit
|
||||||
|
logger.L.Info(sig, ", exitting...")
|
||||||
|
defer logger.L.Info("Bye!")
|
||||||
|
if config.Cfg.NoCleanCache {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.Cfg.Temp.BasePath != "" {
|
||||||
|
for _, path := range []string{"/", ".", "\\", ".."} {
|
||||||
|
if filepath.Clean(config.Cfg.Temp.BasePath) == path {
|
||||||
|
logger.L.Error("Invalid cache dir: ", config.Cfg.Temp.BasePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Error("Failed to get current dir: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cachePath := filepath.Join(currentDir, config.Cfg.Temp.BasePath)
|
||||||
|
cachePath, err = filepath.Abs(cachePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Error("Failed to get absolute path: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.L.Info("Cleaning cache dir: ", cachePath)
|
||||||
|
if err := os.RemoveAll(cachePath); err != nil {
|
||||||
|
logger.L.Error("Failed to clean cache dir: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
60
common/cache.go
Normal file
60
common/cache.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/coocood/freecache"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommonCache struct {
|
||||||
|
cache *freecache.Cache
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var Cache *CommonCache
|
||||||
|
|
||||||
|
func initCache() {
|
||||||
|
gob.Register(types.File{})
|
||||||
|
gob.Register(tg.InputDocumentFileLocation{})
|
||||||
|
gob.Register(tg.InputPhotoFileLocation{})
|
||||||
|
Cache = &CommonCache{cache: freecache.NewCache(10 * 1024 * 1024)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommonCache) Get(key string, value *types.File) error {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
data, err := Cache.cache.Get([]byte(key))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dec := gob.NewDecoder(bytes.NewReader(data))
|
||||||
|
err = dec.Decode(&value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommonCache) Set(key string, value *types.File, expireSeconds int) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
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 {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
Cache.cache.Del([]byte(key))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
initClient()
|
initCache()
|
||||||
}
|
}
|
||||||
|
|||||||
38
common/os.go
38
common/os.go
@@ -18,11 +18,6 @@ func MkFile(path string, data []byte) error {
|
|||||||
return os.WriteFile(path, data, os.ModePerm)
|
return os.WriteFile(path, data, os.ModePerm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FileExists(path string) bool {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除文件, 并清理空目录. 如果文件不存在则返回 nil
|
// 删除文件, 并清理空目录. 如果文件不存在则返回 nil
|
||||||
func PurgeFile(path string) error {
|
func PurgeFile(path string) error {
|
||||||
if err := os.Remove(path); err != nil {
|
if err := os.Remove(path); err != nil {
|
||||||
@@ -33,6 +28,18 @@ func PurgeFile(path string) error {
|
|||||||
return RemoveEmptyDirectories(filepath.Dir(path))
|
return RemoveEmptyDirectories(filepath.Dir(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RmFileAfter(path string, td time.Duration) {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to create timer for %s: %s", path, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.L.Debugf("Remove file after %s: %s", td, path)
|
||||||
|
time.AfterFunc(td, func() {
|
||||||
|
PurgeFile(path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 递归删除空目录
|
// 递归删除空目录
|
||||||
func RemoveEmptyDirectories(dirPath string) error {
|
func RemoveEmptyDirectories(dirPath string) error {
|
||||||
entries, err := os.ReadDir(dirPath)
|
entries, err := os.ReadDir(dirPath)
|
||||||
@@ -48,24 +55,3 @@ func RemoveEmptyDirectories(dirPath string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在指定时间后删除和清理文件 (定时器)
|
|
||||||
func PurgeFileAfter(path string, td time.Duration) {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
logger.L.Errorf("Failed to create timer for %s: %s", path, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.L.Debugf("Purge file after %s: %s", td, path)
|
|
||||||
time.AfterFunc(td, func() {
|
|
||||||
PurgeFile(path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func MkCache(path string, data []byte, td time.Duration) {
|
|
||||||
if err := MkFile(path, data); err != nil {
|
|
||||||
logger.L.Errorf("failed to save cache file: %s", err)
|
|
||||||
} else {
|
|
||||||
go PurgeFileAfter(path, td)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ReqClient *req.Client
|
|
||||||
|
|
||||||
func initClient() {
|
|
||||||
ReqClient = req.NewClient().SetOutputDirectory(config.Cfg.Temp.BasePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDownloadedFilePath(filename string) string {
|
|
||||||
return filepath.Join(config.Cfg.Temp.BasePath, filename)
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,42 @@
|
|||||||
|
workers = 4 # 同时下载文件数
|
||||||
|
retry = 3 # 下载失败重试次数
|
||||||
|
|
||||||
[telegram]
|
[telegram]
|
||||||
token = ""
|
token = "" # Bot Token
|
||||||
admins = []
|
admins = [777000] # 你的 user_id
|
||||||
|
app_id = 123456 # Telegram API ID
|
||||||
|
app_hash = "0123456789abcdef0123456789abcdef" # Telegram API Hash
|
||||||
|
|
||||||
|
[telegram.proxy]
|
||||||
|
enable = false
|
||||||
|
url = "socks5://127.0.0.1:7890" # 代理地址
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
level = "DEBUG"
|
level = "DEBUG" # 日志等级
|
||||||
|
|
||||||
[temp]
|
[temp]
|
||||||
base_path = "cache/"
|
base_path = "cache/" # 下载文件临时目录, 请不要在此目录下存放任何其他文件
|
||||||
cache_ttl = 30
|
cache_ttl = 30 # 临时文件保存时间, 单位: 秒
|
||||||
|
|
||||||
|
[db]
|
||||||
|
path = "data/data.db" # 数据库文件路径
|
||||||
|
|
||||||
[storage]
|
[storage]
|
||||||
[storage.alist]
|
[storage.alist] # Alist
|
||||||
|
enable = true
|
||||||
|
base_path = "/telegram" # 保存路径
|
||||||
|
username = "admin" # 用户名
|
||||||
|
password = "password" # 密码
|
||||||
|
url = "https://alist.com" # Alist 地址
|
||||||
|
token_exp = 86400 # token 过期时间, 单位: 秒
|
||||||
|
|
||||||
|
[storage.local] # 本地磁盘
|
||||||
|
enable = true
|
||||||
|
base_path = "downloads/" # 保存路径
|
||||||
|
|
||||||
|
[storage.webdav] # WebDav
|
||||||
enable = true
|
enable = true
|
||||||
base_path = "/telegram"
|
base_path = "/telegram"
|
||||||
username = "admin"
|
username = "admin"
|
||||||
password = "password"
|
password = "password"
|
||||||
url = "https://alist.com"
|
url = "https://alist.com/dav"
|
||||||
token_exp = 86400
|
|
||||||
|
|
||||||
[storage.local]
|
|
||||||
enable = true
|
|
||||||
base_path = "downloads/"
|
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Workers int `toml:"workers" mapstructure:"workers"`
|
||||||
|
Retry int `toml:"retry" mapstructure:"retry"`
|
||||||
|
NoCleanCache bool `toml:"no_clean_cache" mapstructure:"no_clean_cache"`
|
||||||
|
|
||||||
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
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"`
|
||||||
@@ -31,14 +36,22 @@ type dbConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type telegramConfig struct {
|
type telegramConfig struct {
|
||||||
Token string `toml:"token" mapstructure:"token"`
|
Token string `toml:"token" mapstructure:"token"`
|
||||||
API string `toml:"api" mapstructure:"api"`
|
AppID int `toml:"app_id" mapstructure:"app_id"`
|
||||||
Admins []int64 `toml:"admins" mapstructure:"admins"`
|
AppHash string `toml:"app_hash" mapstructure:"app_hash"`
|
||||||
|
Admins []int64 `toml:"admins" mapstructure:"admins"`
|
||||||
|
Proxy proxyConfig `toml:"proxy" mapstructure:"proxy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type proxyConfig struct {
|
||||||
|
Enable bool `toml:"enable" mapstructure:"enable"`
|
||||||
|
URL string `toml:"url" mapstructure:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type storageConfig struct {
|
type storageConfig struct {
|
||||||
Alist alistConfig `toml:"alist" mapstructure:"alist"`
|
Alist alistConfig `toml:"alist" mapstructure:"alist"`
|
||||||
Local localConfig `toml:"local" mapstructure:"local"`
|
Local localConfig `toml:"local" mapstructure:"local"`
|
||||||
|
Webdav webdavConfig `toml:"webdav" mapstructure:"webdav"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type alistConfig struct {
|
type alistConfig struct {
|
||||||
@@ -55,12 +68,28 @@ type localConfig struct {
|
|||||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type webdavConfig struct {
|
||||||
|
Enable bool `toml:"enable" mapstructure:"enable"`
|
||||||
|
URL string `toml:"url" mapstructure:"url"`
|
||||||
|
Username string `toml:"username" mapstructure:"username"`
|
||||||
|
Password string `toml:"password" mapstructure:"password"`
|
||||||
|
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
||||||
|
}
|
||||||
|
|
||||||
var Cfg *Config
|
var Cfg *Config
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
viper.SetConfigName("config")
|
viper.SetConfigName("config")
|
||||||
viper.AddConfigPath(".")
|
viper.AddConfigPath(".")
|
||||||
|
viper.AddConfigPath("/etc/saveany/")
|
||||||
viper.SetConfigType("toml")
|
viper.SetConfigType("toml")
|
||||||
|
viper.SetEnvPrefix("SAVEANY")
|
||||||
|
viper.AutomaticEnv()
|
||||||
|
replacer := strings.NewReplacer(".", "_")
|
||||||
|
viper.SetEnvKeyReplacer(replacer)
|
||||||
|
|
||||||
|
viper.SetDefault("workers", 3)
|
||||||
|
viper.SetDefault("retry", 3)
|
||||||
|
|
||||||
viper.SetDefault("temp.base_path", "cache/")
|
viper.SetDefault("temp.base_path", "cache/")
|
||||||
viper.SetDefault("temp.cache_ttl", 3600)
|
viper.SetDefault("temp.cache_ttl", 3600)
|
||||||
@@ -71,8 +100,6 @@ func Init() {
|
|||||||
|
|
||||||
viper.SetDefault("db.path", "data/saveany.db")
|
viper.SetDefault("db.path", "data/saveany.db")
|
||||||
|
|
||||||
viper.SetDefault("telegram.api", "https://api.telegram.org")
|
|
||||||
|
|
||||||
viper.SetDefault("storage.alist.base_path", "/")
|
viper.SetDefault("storage.alist.base_path", "/")
|
||||||
viper.SetDefault("storage.alist.token_exp", 3600)
|
viper.SetDefault("storage.alist.token_exp", 3600)
|
||||||
|
|
||||||
@@ -86,4 +113,8 @@ func Init() {
|
|||||||
fmt.Println("Error unmarshalling config file, ", err)
|
fmt.Println("Error unmarshalling config file, ", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if Cfg.Workers < 1 || Cfg.Retry < 1 {
|
||||||
|
fmt.Println("Invalid workers or retry value")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
174
core/core.go
174
core/core.go
@@ -4,90 +4,133 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/imroc/req/v3"
|
"github.com/celestix/gotgproto/ext"
|
||||||
|
"github.com/duke-git/lancet/v2/fileutil"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/bot"
|
"github.com/krau/SaveAny-Bot/bot"
|
||||||
"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/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
"github.com/krau/SaveAny-Bot/queue"
|
"github.com/krau/SaveAny-Bot/queue"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
"github.com/mymmrac/telego"
|
|
||||||
"github.com/mymmrac/telego/telegoutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func processPendingTask(task types.Task) error {
|
func processPendingTask(task *types.Task) error {
|
||||||
fileRecord, err := dao.GetReceivedFileByFileID(task.FileID)
|
logger.L.Debugf("Start processing task: %s", task.String())
|
||||||
|
destPath := filepath.Join(config.Cfg.Temp.BasePath, task.FileName())
|
||||||
|
absDestPath, err := filepath.Abs(destPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("Failed to get absolute path: %w", err)
|
||||||
}
|
}
|
||||||
fileRecord.Processing = true
|
if err := fileutil.CreateDir(filepath.Dir(absDestPath)); err != nil {
|
||||||
if err := dao.UpdateReceivedFile(fileRecord); err != nil {
|
return fmt.Errorf("Failed to create directory: %w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = common.ReqClient.R().SetContext(task.Ctx).SetOutputFile(task.FileName).SetDownloadCallbackWithInterval(func(info req.DownloadInfo) {
|
ctx := task.Ctx.(*ext.Context)
|
||||||
if info.Response == nil || info.Response.Response == nil || info.Response.Response.StatusCode != 200 {
|
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
||||||
return
|
Message: "正在下载: " + task.FileName(),
|
||||||
}
|
ID: task.ReplyMessageID,
|
||||||
process := fmt.Sprintf("%.2f%%", float64(info.DownloadedSize)/float64(info.Response.ContentLength)*100.0)
|
|
||||||
bot.Bot.EditMessageText(&telego.EditMessageTextParams{
|
|
||||||
ChatID: telegoutil.ID(task.ChatID),
|
|
||||||
MessageID: task.ReplyMessageID,
|
|
||||||
Text: "正在下载文件: " + process,
|
|
||||||
})
|
|
||||||
}, time.Second*time.Duration(3*func() int {
|
|
||||||
if queue.Len() > 0 {
|
|
||||||
return queue.Len()
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}())).Get(bot.FileDownloadURL(task.FilePath))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
bot.Bot.EditMessageText(&telego.EditMessageTextParams{
|
|
||||||
ChatID: telegoutil.ID(task.ChatID),
|
|
||||||
MessageID: task.ReplyMessageID,
|
|
||||||
Text: "下载完成, 正在转存...",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if config.Cfg.Temp.CacheTTL > 0 {
|
|
||||||
common.PurgeFileAfter(common.GetDownloadedFilePath(task.FileName),
|
|
||||||
time.Duration(config.Cfg.Temp.CacheTTL)*time.Second)
|
|
||||||
} else {
|
|
||||||
if err := common.PurgeFile(common.GetDownloadedFilePath(task.FileName)); err != nil {
|
|
||||||
logger.L.Errorf("Failed to purge file: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if task.StoragePath == "" {
|
if task.StoragePath == "" {
|
||||||
task.StoragePath = task.FileName
|
task.StoragePath = task.File.FileName
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := storage.Save(task.Storage, task.Ctx, common.GetDownloadedFilePath(task.FileName), task.StoragePath); err != nil {
|
// process photo
|
||||||
return err
|
if task.File.FileSize == 0 {
|
||||||
|
res, err := bot.Client.API().UploadGetFile(task.Ctx, &tg.UploadGetFileRequest{
|
||||||
|
Location: task.File.Location,
|
||||||
|
Offset: 0,
|
||||||
|
Limit: 1024 * 1024,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to get file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := res.(*tg.UploadFile)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(destPath, result.Bytes, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("Failed to write file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer cleanCacheFile(destPath)
|
||||||
|
|
||||||
|
logger.L.Infof("Downloaded file: %s", destPath)
|
||||||
|
|
||||||
|
return saveFileWithRetry(task, destPath)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
barTotalCount := calculateBarTotalCount(task.File.FileSize)
|
||||||
|
|
||||||
|
progressCallback := func(bytesRead, contentLength int64) {
|
||||||
|
progress := float64(bytesRead) / float64(contentLength) * 100
|
||||||
|
logger.L.Tracef("Downloading %s: %.2f%%", task.String(), progress)
|
||||||
|
if task.File.FileSize < 1024*1024*50 || int(progress)%(100/barTotalCount) != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text := fmt.Sprintf("正在处理下载任务\n文件名: %s\n保存路径: %s\n平均速度: %s\n当前进度: [%s] %.2f%%",
|
||||||
|
task.FileName(),
|
||||||
|
fmt.Sprintf("[%s]:%s", task.Storage, task.StoragePath),
|
||||||
|
getSpeed(bytesRead, task.StartTime),
|
||||||
|
getProgressBar(progress, barTotalCount),
|
||||||
|
progress,
|
||||||
|
)
|
||||||
|
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: text,
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
readCloser, err := NewTelegramReader(task.Ctx, bot.Client, &task.File.Location,
|
||||||
|
0, task.File.FileSize-1, task.File.FileSize,
|
||||||
|
progressCallback, task.File.FileSize/100)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to create reader: %w", err)
|
||||||
|
}
|
||||||
|
defer readCloser.Close()
|
||||||
|
|
||||||
|
dest, err := os.Create(destPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to create file: %w", err)
|
||||||
|
}
|
||||||
|
defer dest.Close()
|
||||||
|
task.StartTime = time.Now()
|
||||||
|
if _, err := io.CopyN(dest, readCloser, task.File.FileSize); err != nil {
|
||||||
|
return fmt.Errorf("Failed to download file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer cleanCacheFile(destPath)
|
||||||
|
|
||||||
|
logger.L.Infof("Downloaded file: %s", destPath)
|
||||||
|
ctx.EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
||||||
|
Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()),
|
||||||
|
ID: task.ReplyMessageID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return saveFileWithRetry(task, destPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
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.FileName)
|
logger.L.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())
|
logger.L.Infof("Processing task: %s", task.String())
|
||||||
if err := processPendingTask(task); err != nil {
|
if err := processPendingTask(&task); err != nil {
|
||||||
logger.L.Errorf("Failed to do task: %s", err)
|
logger.L.Errorf("Failed to do task: %s", err)
|
||||||
task.Error = err
|
task.Error = err
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
|
logger.L.Debugf("Task canceled: %s", task.String())
|
||||||
task.Status = types.Canceled
|
task.Status = types.Canceled
|
||||||
} else {
|
} else {
|
||||||
task.Status = types.Failed
|
task.Status = types.Failed
|
||||||
@@ -98,38 +141,31 @@ func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
|||||||
queue.AddTask(task)
|
queue.AddTask(task)
|
||||||
case types.Succeeded:
|
case types.Succeeded:
|
||||||
logger.L.Infof("Task succeeded: %s", task.String())
|
logger.L.Infof("Task succeeded: %s", task.String())
|
||||||
bot.Bot.EditMessageText(&telego.EditMessageTextParams{
|
task.Ctx.(*ext.Context).EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
||||||
ChatID: telegoutil.ID(task.ChatID),
|
Message: "保存成功\n" + task.FileName(),
|
||||||
MessageID: task.ReplyMessageID,
|
ID: task.ReplyMessageID,
|
||||||
Text: "文件转存完成",
|
|
||||||
})
|
})
|
||||||
if err := dao.DeleteReceivedFileByFileID(task.FileID); err != nil {
|
|
||||||
logger.L.Errorf("Failed to delete received file: %s", err)
|
|
||||||
}
|
|
||||||
case types.Failed:
|
case types.Failed:
|
||||||
logger.L.Errorf("Task failed: %s", task.String())
|
logger.L.Errorf("Task failed: %s", task.String())
|
||||||
bot.Bot.EditMessageText(&telego.EditMessageTextParams{
|
task.Ctx.(*ext.Context).EditMessage(task.ChatID, &tg.MessagesEditMessageRequest{
|
||||||
ChatID: telegoutil.ID(task.ChatID),
|
Message: "文件保存失败\n" + task.Error.Error(),
|
||||||
MessageID: task.ReplyMessageID,
|
ID: task.ReplyMessageID,
|
||||||
Text: "文件转存失败:" + "\n" + task.Error.Error(),
|
|
||||||
})
|
})
|
||||||
if err := dao.DeleteReceivedFileByFileID(task.FileID); err != nil {
|
|
||||||
logger.L.Errorf("Failed to delete received file: %s", err)
|
|
||||||
}
|
|
||||||
case types.Canceled:
|
case types.Canceled:
|
||||||
logger.L.Infof("Task canceled: %s", task.String())
|
logger.L.Infof("Task canceled: %s", task.String())
|
||||||
default:
|
default:
|
||||||
logger.L.Errorf("Unknown task status: %s", task.Status)
|
logger.L.Errorf("Unknown task status: %s", task.Status)
|
||||||
}
|
}
|
||||||
<-semaphore
|
<-semaphore
|
||||||
logger.L.Debugf("Task done: %s", task.FileName)
|
logger.L.Debugf("Task done: %s", task.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run() {
|
func Run() {
|
||||||
logger.L.Info("Start processing tasks...")
|
logger.L.Info("Start processing tasks...")
|
||||||
semaphore := make(chan struct{}, 3)
|
semaphore := make(chan struct{}, config.Cfg.Workers)
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < config.Cfg.Workers; i++ {
|
||||||
go worker(queue.Queue, semaphore)
|
go worker(queue.Queue, semaphore)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
154
core/reader.go
Normal file
154
core/reader.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/celestix/gotgproto"
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type telegramReader struct {
|
||||||
|
client *gotgproto.Client
|
||||||
|
location *tg.InputFileLocationClass
|
||||||
|
bytesread int64
|
||||||
|
chunkSize int64
|
||||||
|
i int64
|
||||||
|
contentLength int64
|
||||||
|
start int64
|
||||||
|
end int64
|
||||||
|
next func() ([]byte, error)
|
||||||
|
progressCallback func(bytesRead, contentLength int64)
|
||||||
|
callbackInterval int64
|
||||||
|
lastProgress int64
|
||||||
|
buffer []byte
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*telegramReader) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *telegramReader) Read(p []byte) (n int, err error) {
|
||||||
|
if r.bytesread == r.contentLength {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.i >= int64(len(r.buffer)) {
|
||||||
|
r.buffer, err = r.next()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(r.buffer) == 0 {
|
||||||
|
r.next = r.partStream()
|
||||||
|
r.buffer, err = r.next()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
r.i = 0
|
||||||
|
}
|
||||||
|
n = copy(p, r.buffer[r.i:])
|
||||||
|
r.i += int64(n)
|
||||||
|
r.bytesread += int64(n)
|
||||||
|
|
||||||
|
if r.progressCallback != nil && (r.bytesread-r.lastProgress >= r.callbackInterval || r.bytesread == r.contentLength) {
|
||||||
|
r.progressCallback(r.bytesread, r.contentLength)
|
||||||
|
r.lastProgress = r.bytesread
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTelegramReader(
|
||||||
|
ctx context.Context,
|
||||||
|
client *gotgproto.Client,
|
||||||
|
location *tg.InputFileLocationClass,
|
||||||
|
start int64,
|
||||||
|
end int64,
|
||||||
|
contentLength int64,
|
||||||
|
progressCallback func(bytesRead, contentLength int64),
|
||||||
|
callbackInterval int64,
|
||||||
|
) (io.ReadCloser, error) {
|
||||||
|
|
||||||
|
r := &telegramReader{
|
||||||
|
ctx: ctx,
|
||||||
|
location: location,
|
||||||
|
client: client,
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
chunkSize: int64(1024 * 1024),
|
||||||
|
contentLength: contentLength,
|
||||||
|
progressCallback: progressCallback,
|
||||||
|
callbackInterval: callbackInterval,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.next = r.partStream()
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *telegramReader) chunk(offset int64, limit int64) ([]byte, error) {
|
||||||
|
var lastError error
|
||||||
|
for i := 0; i < config.Cfg.Retry; i++ {
|
||||||
|
req := &tg.UploadGetFileRequest{
|
||||||
|
Offset: offset,
|
||||||
|
Limit: int(limit),
|
||||||
|
Location: *r.location,
|
||||||
|
}
|
||||||
|
res, err := r.client.API().UploadGetFile(r.ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), tg.ErrTimeout) {
|
||||||
|
lastError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch result := res.(type) {
|
||||||
|
case *tg.UploadFile:
|
||||||
|
return result.Bytes, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected type %T", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *telegramReader) partStream() func() ([]byte, error) {
|
||||||
|
|
||||||
|
start := r.start
|
||||||
|
end := r.end
|
||||||
|
offset := start - (start % r.chunkSize)
|
||||||
|
|
||||||
|
firstPartCut := start - offset
|
||||||
|
lastPartCut := (end % r.chunkSize) + 1
|
||||||
|
partCount := int((end - offset + r.chunkSize) / r.chunkSize)
|
||||||
|
currentPart := 1
|
||||||
|
|
||||||
|
readData := func() ([]byte, error) {
|
||||||
|
if currentPart > partCount {
|
||||||
|
return make([]byte, 0), nil
|
||||||
|
}
|
||||||
|
res, err := r.chunk(offset, r.chunkSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(res) == 0 {
|
||||||
|
return res, nil
|
||||||
|
} else if partCount == 1 {
|
||||||
|
res = res[firstPartCut:lastPartCut]
|
||||||
|
} else if currentPart == 1 {
|
||||||
|
res = res[firstPartCut:]
|
||||||
|
} else if currentPart == partCount {
|
||||||
|
res = res[:lastPartCut]
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPart++
|
||||||
|
offset += r.chunkSize
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
return readData
|
||||||
|
}
|
||||||
71
core/utils.go
Normal file
71
core/utils.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/common"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func saveFileWithRetry(task *types.Task, destPath string) error {
|
||||||
|
for i := 0; i <= config.Cfg.Retry; i++ {
|
||||||
|
if err := storage.Save(task.Storage, task.Ctx, destPath, task.StoragePath); err != nil {
|
||||||
|
if i == config.Cfg.Retry {
|
||||||
|
return fmt.Errorf("Failed to save file: %w", err)
|
||||||
|
}
|
||||||
|
logger.L.Errorf("Failed to save file: %s, retrying...", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProgressBar(progress float64, totalCount int) string {
|
||||||
|
bar := ""
|
||||||
|
barSize := 100 / totalCount
|
||||||
|
for i := 0; i < totalCount; i++ {
|
||||||
|
if int(progress)/barSize > i {
|
||||||
|
bar += "█"
|
||||||
|
} else {
|
||||||
|
bar += "░"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bar
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanCacheFile(destPath string) {
|
||||||
|
if config.Cfg.Temp.CacheTTL > 0 {
|
||||||
|
common.RmFileAfter(destPath, time.Duration(config.Cfg.Temp.CacheTTL)*time.Second)
|
||||||
|
} else {
|
||||||
|
if err := os.Remove(destPath); err != nil {
|
||||||
|
logger.L.Errorf("Failed to purge file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateBarTotalCount(fileSize int64) int {
|
||||||
|
barTotalCount := 5
|
||||||
|
if fileSize > 1024*1024*1000 {
|
||||||
|
barTotalCount = 40
|
||||||
|
} else if fileSize > 1024*1024*500 {
|
||||||
|
barTotalCount = 20
|
||||||
|
} else if fileSize > 1024*1024*200 {
|
||||||
|
barTotalCount = 10
|
||||||
|
}
|
||||||
|
return barTotalCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSpeed(bytesRead int64, startTime time.Time) string {
|
||||||
|
if startTime.IsZero() {
|
||||||
|
return "0MB/s"
|
||||||
|
}
|
||||||
|
elapsed := time.Since(startTime)
|
||||||
|
speed := float64(bytesRead) / 1024 / 1024 / elapsed.Seconds()
|
||||||
|
return fmt.Sprintf("%.2fMB/s", speed)
|
||||||
|
}
|
||||||
17
dao/db.go
17
dao/db.go
@@ -3,12 +3,14 @@ package dao
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
"github.com/krau/SaveAny-Bot/model"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
glogger "gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
@@ -19,13 +21,22 @@ func Init() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
db, err = gorm.Open(sqlite.Open(config.Cfg.DB.Path), &gorm.Config{})
|
db, err = gorm.Open(sqlite.Open(config.Cfg.DB.Path), &gorm.Config{
|
||||||
|
Logger: glogger.New(logger.L, glogger.Config{
|
||||||
|
Colorful: true,
|
||||||
|
SlowThreshold: time.Second * 5,
|
||||||
|
LogLevel: glogger.Error,
|
||||||
|
IgnoreRecordNotFoundError: true,
|
||||||
|
ParameterizedQueries: true,
|
||||||
|
}),
|
||||||
|
PrepareStmt: true,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.L.Fatal("Failed to open database: ", err)
|
logger.L.Fatal("Failed to open database: ", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
logger.L.Debug("Database connected")
|
logger.L.Debug("Database connected")
|
||||||
db.AutoMigrate(&model.ReceivedFile{}, &model.User{})
|
db.AutoMigrate(&types.ReceivedFile{}, &types.User{})
|
||||||
|
|
||||||
for _, admin := range config.Cfg.Telegram.Admins {
|
for _, admin := range config.Cfg.Telegram.Admins {
|
||||||
CreateUser(int64(admin))
|
CreateUser(int64(admin))
|
||||||
|
|||||||
51
dao/file.go
51
dao/file.go
@@ -1,31 +1,17 @@
|
|||||||
package dao
|
package dao
|
||||||
|
|
||||||
import "github.com/krau/SaveAny-Bot/model"
|
import "github.com/krau/SaveAny-Bot/types"
|
||||||
|
|
||||||
func AddReceivedFile(receivedFile *model.ReceivedFile) error {
|
func SaveReceivedFile(receivedFile *types.ReceivedFile) error {
|
||||||
return db.Create(receivedFile).Error
|
record, err := GetReceivedFileByChatAndMessageID(receivedFile.ChatID, receivedFile.MessageID)
|
||||||
}
|
if err == nil {
|
||||||
|
receivedFile.ID = record.ID
|
||||||
func GetReceivedFileByFileID(fileID string) (*model.ReceivedFile, error) {
|
|
||||||
var receivedFile model.ReceivedFile
|
|
||||||
err := db.Where("file_id = ?", fileID).First(&receivedFile).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return &receivedFile, nil
|
return db.Save(receivedFile).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetReceivedFileByFileUniqueID(fileUniqueID string) (*model.ReceivedFile, error) {
|
func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*types.ReceivedFile, error) {
|
||||||
var receivedFile model.ReceivedFile
|
var receivedFile types.ReceivedFile
|
||||||
err := db.Where("file_unique_id = ?", fileUniqueID).First(&receivedFile).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &receivedFile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*model.ReceivedFile, error) {
|
|
||||||
var receivedFile model.ReceivedFile
|
|
||||||
err := db.Where("chat_id = ? AND message_id = ?", chatID, messageID).First(&receivedFile).Error
|
err := db.Where("chat_id = ? AND message_id = ?", chatID, messageID).First(&receivedFile).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -33,23 +19,6 @@ func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*model.Rece
|
|||||||
return &receivedFile, nil
|
return &receivedFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetReceivedFilesByMediaGroupID(mediaGroupID string) ([]model.ReceivedFile, error) {
|
func DeleteReceivedFile(receivedFile *types.ReceivedFile) error {
|
||||||
var receivedFiles []model.ReceivedFile
|
return db.Delete(receivedFile).Error
|
||||||
err := db.Where("media_group_id = ?", mediaGroupID).Find(&receivedFiles).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return receivedFiles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateReceivedFile(receivedFile *model.ReceivedFile) error {
|
|
||||||
return db.Save(receivedFile).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteReceivedFileByFileID(fileID string) error {
|
|
||||||
return db.Where("file_id = ?", fileID).Delete(&model.ReceivedFile{}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteReceivedFileByFileUniqueID(fileUniqueID string) error {
|
|
||||||
return db.Where("file_unique_id = ?", fileUniqueID).Delete(&model.ReceivedFile{}).Error
|
|
||||||
}
|
}
|
||||||
|
|||||||
10
dao/user.go
10
dao/user.go
@@ -1,22 +1,22 @@
|
|||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/krau/SaveAny-Bot/model"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateUser(userID int64) error {
|
func CreateUser(userID int64) error {
|
||||||
if _, err := GetUserByUserID(userID); err == nil {
|
if _, err := GetUserByUserID(userID); err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return db.Create(&model.User{UserID: userID}).Error
|
return db.Create(&types.User{UserID: userID}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserByUserID(userID int64) (*model.User, error) {
|
func GetUserByUserID(userID int64) (*types.User, error) {
|
||||||
var user model.User
|
var user types.User
|
||||||
err := db.Where("user_id = ?", userID).First(&user).Error
|
err := db.Where("user_id = ?", userID).First(&user).Error
|
||||||
return &user, err
|
return &user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateUser(user *model.User) error {
|
func UpdateUser(user *types.User) error {
|
||||||
return db.Save(user).Error
|
return db.Save(user).Error
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
docs/logo.jpg
Normal file
BIN
docs/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
122
go.mod
122
go.mod
@@ -1,95 +1,99 @@
|
|||||||
module github.com/krau/SaveAny-Bot
|
module github.com/krau/SaveAny-Bot
|
||||||
|
|
||||||
go 1.23.2
|
go 1.23.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blang/semver v3.5.1+incompatible
|
github.com/blang/semver v3.5.1+incompatible
|
||||||
github.com/gookit/slog v0.5.6
|
github.com/celestix/gotgproto v1.0.0-beta20.2
|
||||||
github.com/imroc/req/v3 v3.46.1
|
github.com/gookit/slog v0.5.7
|
||||||
github.com/mymmrac/telego v0.31.3
|
github.com/gotd/contrib v0.21.0
|
||||||
|
github.com/gotd/td v0.118.0
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
|
github.com/studio-b12/gowebdav v0.10.0
|
||||||
|
golang.org/x/net v0.35.0
|
||||||
|
golang.org/x/time v0.10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cloudflare/circl v1.4.0 // indirect
|
github.com/AnimeKaizoku/cacher v1.0.2 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/coder/websocket v1.8.12 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
|
github.com/go-faster/errors v0.7.1 // indirect
|
||||||
|
github.com/go-faster/jx v1.1.0 // indirect
|
||||||
|
github.com/go-faster/xor v1.0.0 // indirect
|
||||||
|
github.com/go-faster/yaml v0.4.6 // 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.0.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20241009165004-a3522334989c // indirect
|
github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/gotd/ige v0.2.2 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // 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/mattn/go-isatty v0.0.17 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/quic-go/quic-go v0.47.0 // indirect
|
github.com/ogen-go/ogen v1.10.0 // indirect
|
||||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
github.com/onsi/gomega v1.36.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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.9 // indirect
|
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||||
go.uber.org/mock v0.4.0 // indirect
|
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||||
golang.org/x/crypto v0.28.0 // indirect
|
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||||
golang.org/x/mod v0.21.0 // indirect
|
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||||
golang.org/x/net v0.30.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/oauth2 v0.18.0 // indirect
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
golang.org/x/tools v0.26.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
golang.org/x/mod v0.23.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
golang.org/x/oauth2 v0.26.0 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
golang.org/x/tools v0.30.0 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
modernc.org/libc v1.61.13 // indirect
|
||||||
modernc.org/sqlite v1.23.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.8.2 // indirect
|
||||||
|
modernc.org/sqlite v1.34.5 // indirect
|
||||||
|
rsc.io/qr v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/coocood/freecache v1.2.4
|
||||||
github.com/bytedance/sonic v1.12.2 // indirect
|
github.com/duke-git/lancet/v2 v2.3.4
|
||||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
|
||||||
github.com/duke-git/lancet/v2 v2.3.3
|
|
||||||
github.com/fasthttp/router v1.5.2 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.7.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.15
|
github.com/gookit/goutil v0.6.18
|
||||||
github.com/gookit/gsr v0.1.0 // indirect
|
github.com/gookit/gsr v0.1.0 // indirect
|
||||||
github.com/grbit/go-json v0.11.0 // 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.10 // indirect
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
github.com/magiconair/properties v1.8.9 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.12.0 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.7.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.55.0 // indirect
|
|
||||||
github.com/valyala/fastjson v1.6.4 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
|
||||||
golang.org/x/arch v0.6.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
|
||||||
golang.org/x/text v0.19.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/gorm v1.25.12
|
gorm.io/gorm v1.25.12
|
||||||
|
|||||||
365
go.sum
365
go.sum
@@ -1,79 +1,91 @@
|
|||||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
github.com/AnimeKaizoku/cacher v1.0.2 h1:7Bf5qRylWb7q2Evib0OXlhG37/t7BP2HK/7IyPvSmGQ=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
github.com/AnimeKaizoku/cacher v1.0.2/go.mod h1:jw0de/b0K6W7Y3T9rHCMGVKUf6oG7hENNcssxYcZTCc=
|
||||||
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/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=
|
github.com/celestix/gotgproto v1.0.0-beta20.1 h1:F7H08CuSiHP0YlZqATBi2wJvg7dxXFvFbpauWFd0IbI=
|
||||||
github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
github.com/celestix/gotgproto v1.0.0-beta20.1/go.mod h1:j42ZhBMUke6QyBLvCgx8tA+TL9L3+pq/Q46B+b5+3aU=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/celestix/gotgproto v1.0.0-beta20.2 h1:+WcsKdsyj4xy+TAV+4Sw6zp1xiQrIr4dMnM31+k8NYM=
|
||||||
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
github.com/celestix/gotgproto v1.0.0-beta20.2/go.mod h1:j42ZhBMUke6QyBLvCgx8tA+TL9L3+pq/Q46B+b5+3aU=
|
||||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
|
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
|
||||||
|
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
|
||||||
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/davecgh/go-spew v1.1.0/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.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=
|
||||||
github.com/duke-git/lancet/v2 v2.3.3 h1:OhqzNzkbJBS9ZlWLo/C7g+WSAOAAyNj7p9CAiEHurUc=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/duke-git/lancet/v2 v2.3.3/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
|
||||||
|
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
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/fasthttp/router v1.5.2 h1:ckJCCdV7hWkkrMeId3WfEhz+4Gyyf6QPwxi/RHIMZ6I=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fasthttp/router v1.5.2/go.mod h1:C8EY53ozOwpONyevc/V7Gr8pqnEjwnkFFqPo1alAGs0=
|
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=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
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.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
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/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
|
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||||
|
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||||
|
github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg=
|
||||||
|
github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
|
||||||
|
github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
|
||||||
|
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/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
|
||||||
|
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
||||||
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-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
||||||
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
||||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/pprof v0.0.0-20241009165004-a3522334989c h1:NDovD0SMpBYXlE1zJmS1q55vWB/fUQBcPAqAboZSccA=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
github.com/google/pprof v0.0.0-20241009165004-a3522334989c/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
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/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
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=
|
||||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||||
github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
|
github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw=
|
||||||
github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
|
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/slog v0.5.6 h1:fmh+7bfOK8CjidMCwE+M3S8G766oHJpT/1qdmXGALCI=
|
github.com/gookit/slog v0.5.7 h1:n3Dhgmr3NP+KppkNg95+vpFcI4YD1csu9VTQwgcEYTs=
|
||||||
github.com/gookit/slog v0.5.6/go.mod h1:RfIwzoaQ8wZbKdcqG7+3EzbkMqcp2TUn3mcaSZAw2EQ=
|
github.com/gookit/slog v0.5.7/go.mod h1:uWCRB4YO+FmgwXEq3s8U7ob1wWP7RStOuY/2a4yC/8o=
|
||||||
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
|
github.com/gotd/contrib v0.21.0 h1:4Fj05jnyBE84toXZl7mVTvt7f732n5uglvztyG6nTr4=
|
||||||
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
|
github.com/gotd/contrib v0.21.0/go.mod h1:ENoUh75IhHGxfz/puVJg8BU4ZF89yrL6Q47TyoNqFYo=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
github.com/gotd/td v0.118.0 h1:iPGkaOAd3QO72TcvzNJGKGpLDzYOW8GIz+Va2upxBbY=
|
||||||
|
github.com/gotd/td v0.118.0/go.mod h1:FUNVeJB9Id2Vqps9yF+8kmBNNyCGO6VXDyO8Ah7bVSw=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/imroc/req/v3 v3.46.1 h1:oahr2hBTb3AaFI4P6jkN0Elj2ZVKJcdQ/IjWqeIKjvc=
|
|
||||||
github.com/imroc/req/v3 v3.46.1/go.mod h1:weam9gmyb00QnOtu6HXSnk44dNFkIUQb5QdMx13FeUU=
|
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
@@ -82,12 +94,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
|||||||
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.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
|
||||||
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=
|
||||||
@@ -95,184 +103,195 @@ 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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
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/mymmrac/telego v0.31.3 h1:yZlD+dm+1W6p3OmCG8K+MbS02Y6paUgwPnqfZN3RWQQ=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/mymmrac/telego v0.31.3/go.mod h1:coOoqXVmjFnwBlzusjfEezbQ7RH9wQnDowJdMm+bnEo=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/ogen-go/ogen v1.9.0 h1:n+lDQpiSFYC9G4hTvuNVWnqmIP0LR8ws0faDn9jX3hU=
|
||||||
|
github.com/ogen-go/ogen v1.9.0/go.mod h1:vkHpuRyzjdfuRCy81EShi4t9sIgZDcNPGmiDKipRloc=
|
||||||
|
github.com/ogen-go/ogen v1.10.0 h1:x3ukRtq/pdn/k8+pYBtqWceVASiSmgK9M5lrH89Q+04=
|
||||||
|
github.com/ogen-go/ogen v1.10.0/go.mod h1:WExXrswerPzGWD0NpzBFsz+5eQIbP7HAtZUmpV8dqqI=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
|
|
||||||
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.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
|
||||||
github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y=
|
|
||||||
github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E=
|
|
||||||
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
|
||||||
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
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.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||||
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/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
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.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.7.1/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/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|
||||||
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/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/studio-b12/gowebdav v0.10.0 h1:Yewz8FFiadcGEu4hxS/AAJQlHelndqln1bns3hcJIYc=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/studio-b12/gowebdav v0.10.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.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=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
|
||||||
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
|
|
||||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
|
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||||
|
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
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/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
|
|
||||||
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
|
|
||||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
|
||||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
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/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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
|
||||||
|
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||||
|
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
|
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||||
|
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-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.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
|
||||||
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.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||||
|
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
|
||||||
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||||
|
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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=
|
||||||
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/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
|
||||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
|
||||||
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
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=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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.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/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
modernc.org/ccgo/v4 v4.23.15 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc=
|
||||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
modernc.org/ccgo/v4 v4.23.15/go.mod h1:nJX30dks/IWuBOnVa7VRii9Me4/9TZ1SC9GNtmARTy0=
|
||||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
||||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
modernc.org/gc/v2 v2.6.2 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
||||||
|
modernc.org/libc v1.61.11 h1:6sZG8uB6EMMG7iTLPTndi8jyTdgAQNIeLGjCFICACZw=
|
||||||
|
modernc.org/libc v1.61.11/go.mod h1:HHX+srFdn839oaJRd0W8hBM3eg+mieyZCAjWwB08/nM=
|
||||||
|
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||||
|
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
|
||||||
|
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
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/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
|
||||||
|
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
|
||||||
|
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||||
|
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||||
|
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ReceivedFile struct {
|
|
||||||
gorm.Model
|
|
||||||
// FileUniqueID 和 FileID 在数据库中均不具有唯一性
|
|
||||||
// 如需确定唯一一行, 使用 ChatID + MessageID
|
|
||||||
FileUniqueID string `gorm:"index"`
|
|
||||||
FileID string `gorm:"index"`
|
|
||||||
Processing bool
|
|
||||||
FileName string
|
|
||||||
FilePath string
|
|
||||||
FileSize int64
|
|
||||||
MediaGroupID string
|
|
||||||
ChatID int64
|
|
||||||
MessageID int
|
|
||||||
ReplyMessageID int
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
gorm.Model
|
|
||||||
UserID int64 `gorm:"uniqueIndex"`
|
|
||||||
Silent bool
|
|
||||||
DefaultStorage string
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,35 @@
|
|||||||
package alist
|
package alist
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Alist struct{}
|
type Alist struct {
|
||||||
|
client *http.Client
|
||||||
|
token string
|
||||||
|
basePath string
|
||||||
|
baseURL string
|
||||||
|
loginInfo *loginRequest
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
basePath string
|
|
||||||
baseUrl string
|
|
||||||
reqClient *req.Client
|
|
||||||
loginReq *loginRequset
|
|
||||||
|
|
||||||
ErrAlistLoginFailed = errors.New("failed to login to Alist")
|
ErrAlistLoginFailed = errors.New("failed to login to Alist")
|
||||||
)
|
)
|
||||||
|
|
||||||
type loginRequset struct {
|
type loginRequest struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
@@ -40,70 +42,137 @@ type loginResponse struct {
|
|||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getToken() (string, error) {
|
type putResponse struct {
|
||||||
resp, err := reqClient.R().SetBodyJsonMarshal(loginReq).Post("/api/auth/login")
|
Code int `json:"code"`
|
||||||
if err != nil {
|
Message string `json:"message"`
|
||||||
return "", err
|
Data struct {
|
||||||
}
|
Task struct {
|
||||||
var loginResp loginResponse
|
ID string `json:"id"`
|
||||||
if err := json.Unmarshal(resp.Bytes(), &loginResp); err != nil {
|
Name string `json:"name"`
|
||||||
return "", err
|
State int `json:"state"`
|
||||||
}
|
Status string `json:"status"`
|
||||||
if loginResp.Code != http.StatusOK {
|
Progress int `json:"progress"`
|
||||||
return "", fmt.Errorf("%w: %s", ErrAlistLoginFailed, loginResp.Message)
|
Error string `json:"error"`
|
||||||
}
|
} `json:"task"`
|
||||||
return loginResp.Data.Token, nil
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshToken(client *req.Client) {
|
func (a *Alist) getToken() error {
|
||||||
|
loginBody, err := json.Marshal(a.loginInfo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal login request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, a.baseURL+"/api/auth/login", bytes.NewBuffer(loginBody))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create login request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send login request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read login response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginResp loginResponse
|
||||||
|
if err := json.Unmarshal(body, &loginResp); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal login response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginResp.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("%w: %s", ErrAlistLoginFailed, loginResp.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.token = loginResp.Data.Token
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Alist) refreshToken() {
|
||||||
for {
|
for {
|
||||||
time.Sleep(time.Duration(config.Cfg.Storage.Alist.TokenExp) * time.Second)
|
time.Sleep(time.Duration(config.Cfg.Storage.Alist.TokenExp) * time.Second)
|
||||||
token, err := getToken()
|
if err := a.getToken(); err != nil {
|
||||||
if err != nil {
|
|
||||||
logger.L.Errorf("Failed to refresh jwt token: %v", err)
|
logger.L.Errorf("Failed to refresh jwt token: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
client.SetCommonHeader("Authorization", token)
|
|
||||||
logger.L.Info("Refreshed Alist jwt token")
|
logger.L.Info("Refreshed Alist jwt token")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Alist) Init() {
|
func (a *Alist) Init() {
|
||||||
basePath = config.Cfg.Storage.Alist.BasePath
|
a.basePath = config.Cfg.Storage.Alist.BasePath
|
||||||
baseUrl = config.Cfg.Storage.Alist.URL
|
a.baseURL = config.Cfg.Storage.Alist.URL
|
||||||
reqClient = req.C().SetTLSHandshakeTimeout(time.Second * 10).SetBaseURL(baseUrl)
|
a.client = &http.Client{
|
||||||
loginReq = &loginRequset{
|
Timeout: 12 * time.Hour,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.loginInfo = &loginRequest{
|
||||||
Username: config.Cfg.Storage.Alist.Username,
|
Username: config.Cfg.Storage.Alist.Username,
|
||||||
Password: config.Cfg.Storage.Alist.Password,
|
Password: config.Cfg.Storage.Alist.Password,
|
||||||
}
|
}
|
||||||
token, err := getToken()
|
|
||||||
if err != nil {
|
if err := a.getToken(); err != nil {
|
||||||
logger.L.Fatalf("Failed to login to Alist: %v", err)
|
logger.L.Fatalf("Failed to login to Alist: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
logger.L.Debug("Logged in to Alist")
|
logger.L.Debug("Logged in to Alist")
|
||||||
reqClient.SetCommonHeader("Authorization", token)
|
|
||||||
go refreshToken(reqClient)
|
go a.refreshToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
|
func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
|
||||||
storagePath = path.Join(basePath, storagePath)
|
storagePath = path.Join(a.basePath, storagePath)
|
||||||
file, err := os.Open(filePath)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to open file: %w", err)
|
||||||
}
|
}
|
||||||
resp, err := reqClient.R().
|
defer file.Close()
|
||||||
SetContext(ctx).
|
|
||||||
SetBody(file).
|
filestat, err := file.Stat()
|
||||||
SetHeaders(map[string]string{
|
|
||||||
"File-Path": url.PathEscape(storagePath),
|
|
||||||
"As-Task": "true",
|
|
||||||
}).Put("/api/fs/put")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", a.token)
|
||||||
|
req.Header.Set("File-Path", url.PathEscape(storagePath))
|
||||||
|
req.Header.Set("As-Task", "true")
|
||||||
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
|
req.ContentLength = filestat.Size()
|
||||||
|
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("failed to save file to Alist: %s", resp.Status)
|
return fmt.Errorf("failed to save file to Alist: %s", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var putResp putResponse
|
||||||
|
if err := json.Unmarshal(body, &putResp); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal put response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if putResp.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("failed to save file to Alist: %d, %s", putResp.Code, putResp.Message)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,5 +21,13 @@ func (l *Local) Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Local) Save(ctx context.Context, filePath, storagePath string) error {
|
func (l *Local) Save(ctx context.Context, filePath, storagePath string) error {
|
||||||
return fileutil.CopyFile(filePath, filepath.Join(config.Cfg.Storage.Local.BasePath, storagePath))
|
storagePath = filepath.Join(config.Cfg.Storage.Local.BasePath, storagePath)
|
||||||
|
absPath, err := filepath.Abs(storagePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fileutil.CreateDir(filepath.Dir(absPath)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fileutil.CopyFile(filePath, storagePath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
"github.com/krau/SaveAny-Bot/storage/alist"
|
"github.com/krau/SaveAny-Bot/storage/alist"
|
||||||
"github.com/krau/SaveAny-Bot/storage/local"
|
"github.com/krau/SaveAny-Bot/storage/local"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage/webdav"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ type Storage interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var Storages = make(map[types.StorageType]Storage)
|
var Storages = make(map[types.StorageType]Storage)
|
||||||
|
var StorageKeys = make([]types.StorageType, 0)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
logger.L.Debug("Initializing storage...")
|
logger.L.Debug("Initializing storage...")
|
||||||
@@ -29,6 +32,16 @@ func Init() {
|
|||||||
Storages[types.Local] = new(local.Local)
|
Storages[types.Local] = new(local.Local)
|
||||||
Storages[types.Local].Init()
|
Storages[types.Local].Init()
|
||||||
}
|
}
|
||||||
|
if config.Cfg.Storage.Webdav.Enable {
|
||||||
|
Storages[types.Webdav] = new(webdav.Webdav)
|
||||||
|
Storages[types.Webdav].Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range Storages {
|
||||||
|
StorageKeys = append(StorageKeys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
slice.Sort(StorageKeys)
|
||||||
|
|
||||||
logger.L.Debug("Storage initialized")
|
logger.L.Debug("Storage initialized")
|
||||||
}
|
}
|
||||||
|
|||||||
8
storage/webdav/errs.go
Normal file
8
storage/webdav/errs.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package webdav
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFailedToCreateDirectory = errors.New("webdav: failed to create directory")
|
||||||
|
ErrFailedToWriteFile = errors.New("webdav: failed to write file")
|
||||||
|
)
|
||||||
51
storage/webdav/webdav.go
Normal file
51
storage/webdav/webdav.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package webdav
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
|
"github.com/studio-b12/gowebdav"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Webdav struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
Client *gowebdav.Client
|
||||||
|
basePath string
|
||||||
|
)
|
||||||
|
|
||||||
|
func (w *Webdav) Init() {
|
||||||
|
webdavConfig := config.Cfg.Storage.Webdav
|
||||||
|
basePath = strings.TrimSuffix(webdavConfig.BasePath, "/")
|
||||||
|
Client = gowebdav.NewClient(webdavConfig.URL, webdavConfig.Username, webdavConfig.Password)
|
||||||
|
if err := Client.Connect(); err != nil {
|
||||||
|
logger.L.Fatalf("Failed to connect to webdav server: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
Client.SetTimeout(24 * time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error {
|
||||||
|
storagePath = path.Join(basePath, storagePath)
|
||||||
|
if err := Client.MkdirAll(path.Dir(storagePath), os.ModePerm); err != nil {
|
||||||
|
logger.L.Errorf("Failed to create directory %s: %v", path.Dir(storagePath), err)
|
||||||
|
return ErrFailedToCreateDirectory
|
||||||
|
}
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to open file %s: %v", filePath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if err := Client.WriteStream(storagePath, file, os.ModePerm); err != nil {
|
||||||
|
logger.L.Errorf("Failed to write file %s: %v", storagePath, err)
|
||||||
|
return ErrFailedToWriteFile
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
21
types/model.go
Normal file
21
types/model.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReceivedFile struct {
|
||||||
|
gorm.Model
|
||||||
|
Processing bool
|
||||||
|
ChatID int64 `gorm:"uniqueIndex:idx_chat_id_message_id;not null"`
|
||||||
|
MessageID int `gorm:"uniqueIndex:idx_chat_id_message_id;not null"`
|
||||||
|
ReplyMessageID int
|
||||||
|
FileName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
gorm.Model
|
||||||
|
UserID int64 `gorm:"uniqueIndex"`
|
||||||
|
Silent bool
|
||||||
|
DefaultStorage string
|
||||||
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotd/td/tg"
|
||||||
|
)
|
||||||
|
|
||||||
type TaskStatus string
|
type TaskStatus string
|
||||||
|
|
||||||
@@ -16,26 +22,36 @@ type StorageType string
|
|||||||
var (
|
var (
|
||||||
StorageAll StorageType = "all"
|
StorageAll StorageType = "all"
|
||||||
Local StorageType = "local"
|
Local StorageType = "local"
|
||||||
|
Webdav StorageType = "webdav"
|
||||||
Alist StorageType = "alist"
|
Alist StorageType = "alist"
|
||||||
)
|
)
|
||||||
|
|
||||||
var StorageTypes = []StorageType{Local, Alist, StorageAll}
|
var StorageTypes = []StorageType{Local, Alist, Webdav, StorageAll}
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
FileID string
|
|
||||||
Error error
|
Error error
|
||||||
Status TaskStatus
|
Status TaskStatus
|
||||||
FilePath string // telegram File object's FilePath
|
File *File
|
||||||
FileName string
|
|
||||||
Storage StorageType
|
Storage StorageType
|
||||||
StoragePath string
|
StoragePath string
|
||||||
|
StartTime time.Time
|
||||||
|
|
||||||
// For track progress
|
MessageID int
|
||||||
ChatID int64
|
ChatID int64
|
||||||
ReplyMessageID int
|
ReplyMessageID int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) String() string {
|
func (t Task) String() string {
|
||||||
return t.FileName
|
return fmt.Sprintf("[%d:%d]:%s", t.ChatID, t.MessageID, t.File.FileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Task) FileName() string {
|
||||||
|
return t.File.FileName
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Location tg.InputFileLocationClass
|
||||||
|
FileSize int64
|
||||||
|
FileName string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user