Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95f7d5abb5 | ||
|
|
5b8249060d | ||
|
|
0b97d15b2a | ||
|
|
6db5633ca6 | ||
|
|
4ccbf8177e | ||
|
|
6e16dc6051 | ||
|
|
9ed2c425be | ||
|
|
bc892d9370 | ||
|
|
29d523bd4f | ||
|
|
3f9c3f2a73 | ||
|
|
b796b045a8 | ||
|
|
5f7b936270 | ||
|
|
df64ec3069 | ||
|
|
d3cc56c8e6 | ||
|
|
018ed47949 | ||
|
|
09f4dd4ce7 |
86
.github/copilot-instructions.md
vendored
Normal file
86
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# SaveAny-Bot AI 协作说明
|
||||||
|
|
||||||
|
本项目是一个将 Telegram 文件/消息转存到多种存储端的 Bot,主要用 Go 实现,CLI 入口在 `cmd/`,核心逻辑分布在 `client/`、`core/`、`config/`、`database/`、`storage/` 等目录。下面是针对本仓库的专用约定,供 AI 编码助手参考。
|
||||||
|
|
||||||
|
## 总体架构与入口
|
||||||
|
- **CLI 入口**:
|
||||||
|
- 二进制入口:`main.go` 调用 `cmd.Execute(ctx)`。
|
||||||
|
- 根命令:`cmd/root.go` 使用 `cobra` 定义 `saveany-bot`,`Run` 实现在 `cmd/run.go`。
|
||||||
|
- **应用启动流程(非常重要)**:
|
||||||
|
- `cmd/run.go::Run` 中按顺序完成:读取配置 `config.Init` → 初始化缓存 `common/cache` → 初始化 i18n `common/i18n` → 初始化数据库 `database.Init` → 加载存储 `storage.LoadStorages` → 加载解析器插件 `parsers.LoadPlugins` → (可选)Userbot 登录 → 启动 Telegram Bot `client/bot.Init` → 启动核心任务队列消费 `core.Run`。
|
||||||
|
- 添加新的初始化步骤时,请遵循该顺序并放在 `initAll` 中,而不是分散在各处。
|
||||||
|
|
||||||
|
## 配置与约定
|
||||||
|
- **配置系统**:
|
||||||
|
- 使用 `viper` 读取 `config.toml`,核心结构体定义在 `config/viper.go::Config`。
|
||||||
|
- 默认值在 `config.Init` 中通过 `viper.SetDefault` 定义,如 `workers`、`retry`、`telegram.*`、`db.*` 等。
|
||||||
|
- `Config.C()` 返回的是全局配置副本(值类型),不要在返回值上修改字段;如果需要修改配置流程,应在 `config.Init` 内或通过 `viper` 进行。
|
||||||
|
- 存储配置通过 `config/storage/factory.go::LoadStorageConfigs` 加载并校验,新增存储类型需:
|
||||||
|
- 在 `pkg/enums/storage` 中增加枚举。
|
||||||
|
- 在 `config/storage/` 下新增具体 `StorageConfig` 实现并实现 `Validate`。
|
||||||
|
- 在 `storageFactories` 映射中注册工厂方法。
|
||||||
|
- **环境变量**:
|
||||||
|
- 所有配置键会被 `SAVEANY_` 前缀的环境变量覆盖,`.` 会被 `_` 替换(`SAVEANY_TELEGRAM_APP_ID` 等)。
|
||||||
|
|
||||||
|
## Telegram 客户端与中间件
|
||||||
|
- **Bot 客户端**:
|
||||||
|
- 入口在 `client/bot/bot.go::Init`,使用 `gotgproto.NewClient` 创建,Session 使用 `database.GetDialect(config.C().DB.Session)`,错误处理通过 `ErrorHandler` 回调完成。
|
||||||
|
- Handlers 注册集中在 `client/bot/handlers` 目录,`handlers.Register` 负责统一挂载;新增命令/消息处理逻辑时优先在该目录按功能拆分文件,实现后在 `handlers.Register` 中注册。
|
||||||
|
- Bot 命令列表依赖 `handlers.CommandHandlers` 来自动注册到 Telegram;新增命令时务必更新该切片,以保持 `/help` 与 Bot 命令列表一致。
|
||||||
|
- **中间件**:
|
||||||
|
- 通用中间件位于 `client/middleware/`,包含 floodwait、防崩溃、重试等;`middleware.NewDefaultMiddlewares` 在 `client/bot/bot.go` 中统一挂载。
|
||||||
|
- 新增跨所有更新生效的行为(如日志、统计)时,应优先实现为中间件。
|
||||||
|
|
||||||
|
## 核心任务与队列
|
||||||
|
- **任务接口与队列**:
|
||||||
|
- 核心接口:`core/core.go::Executable`,包含 `Type() TaskType`、`Title()`、`TaskID()`、`Execute(ctx)`。
|
||||||
|
- 任务队列:`pkg/queue.TaskQueue[Executable]`,由 `core.Run` 使用;`Workers` 数量来自配置 `config.C().Workers`。
|
||||||
|
- 任务类型与实现示例位于 `core/tasks/**`,例如文件任务、Telegraph 任务等;新增任务类型应放在对应子目录并实现 `Executable` 接口,然后通过 `core.AddTask` 入队。
|
||||||
|
- **生命周期 Hook**:
|
||||||
|
- `core.worker` 在执行任务前后会根据 `config.C().Hook.Exec` 调用外部命令(`TaskBeforeStart` / `TaskSuccess` / `TaskFail` / `TaskCancel`)。
|
||||||
|
- 修改任务执行流程时需保留这些 Hook 调用,以免破坏用户已有集成。
|
||||||
|
|
||||||
|
## 数据库与持久化
|
||||||
|
- **数据库初始化**:
|
||||||
|
- `database.Init` 使用配置 `config.C().DB.Path` 创建并连接 SQLite,使用 `GetDialect` 抽象驱动(见 `database/driver_*.go`)。
|
||||||
|
- Migration 通过 `db.AutoMigrate(&User{}, &Dir{}, &Rule{}, &WatchChat{})` 完成,模型定义在 `database/*.go` 中。
|
||||||
|
- **用户同步约定**:
|
||||||
|
- `database.syncUsers` 会根据 `config.C().Users` 同步数据库用户表:在配置中新增/删除用户会自动在 DB 中创建/删除对应记录。
|
||||||
|
- 开发涉及用户表逻辑时,请考虑该同步行为,避免在其他地方直接创建/删除用户记录而与配置冲突。
|
||||||
|
|
||||||
|
## 存储后端
|
||||||
|
- **存储抽象**:
|
||||||
|
- 抽象接口在 `config/storage/types.go` 与 `storage/` 顶层(以及子目录)中;`config/storage/*.go` 处理配置解析,`storage/*` 处理真正的上传/下载实现。
|
||||||
|
- 现有实现包括 `local`、`alist`、`s3/minio`、`webdav`、`telegram` 等,每个后端都有对应子目录和配置结构体。
|
||||||
|
- **新增存储实现的推荐路径**:
|
||||||
|
- 在 `config/storage/` 下添加配置结构体 + `Validate`。
|
||||||
|
- 在 `storage/` 下添加具体实现(例如 `storage/foo/`)。
|
||||||
|
- 在 `pkg/enums/storage` 与 `storageFactories` 中注册,并确保 `storages` 配置示例被更新(`config.example.toml` / 文档)。
|
||||||
|
|
||||||
|
## 解析器插件(JS)
|
||||||
|
- **插件运行时**:
|
||||||
|
- 解析器接口和插件文档在 `plugins/README.md`,Go 端入口为 `parsers/` 目录,使用 `goja` 与 `playwright-go`。
|
||||||
|
- 插件通过 `registerParser({ metadata, canHandle, parse })` 注册,使用 `ghttp`/`playwright` 进行 HTTP/浏览器请求。
|
||||||
|
- **与核心交互约定**:
|
||||||
|
- 插件 `parse` 返回的 `Item`/`Resource` 会被转化为内部任务(通常是下载/转存任务)并进入 `core` 队列。
|
||||||
|
- 修改 `Item`/`Resource` 结构或解析逻辑时,要确保保持向后兼容,或在 `plugins/README.md` 中同步更新字段说明和示例。
|
||||||
|
|
||||||
|
## i18n 与日志
|
||||||
|
- **国际化**:
|
||||||
|
- 所有用户可见字符串(尤其是错误与提示)应使用 `common/i18n`:`i18n.T(i18nk.SomeKey, map[string]any{"Name": name})`。
|
||||||
|
- 语言文件位于 `common/i18n/locale/`,`go:generate` 指令在 `main.go` 中生成 `i18nk/keys.go`;新增文案时需:添加到 YAML、运行 `go generate ./...`、再在代码中引用新 key。
|
||||||
|
- **日志**:
|
||||||
|
- 使用 `github.com/charmbracelet/log`,在 `cmd/run.go::Run` 中通过 `log.WithContext` 将 logger 注入 `context.Context`;后续代码优先通过 `log.FromContext(ctx)` 获取 logger。
|
||||||
|
- 编写新代码时,如已有 `ctx`,请使用 `log.FromContext(ctx)` 而不是全局 logger。
|
||||||
|
|
||||||
|
## 开发与运行
|
||||||
|
- **本地运行**:
|
||||||
|
- 直接运行:`go run ./cmd`(`cmd/root.go` + `cmd/run.go`)。
|
||||||
|
- 或通过 Docker:参见根目录 `README.md` 中的 `docker run ...` 示例及 `docker-compose.yml`。
|
||||||
|
- **代码生成与文档**:
|
||||||
|
- i18n key 生成:`go generate ./...` 会执行 `main.go` 顶部的 `//go:generate`,使用 `cmd/geni18n/main.go` 生成 `common/i18n/i18nk/keys.go`。
|
||||||
|
- 文档站点在 `docs/`(Hugo),通常不需要在核心代码改动时同步修改,除非涉及文档内容。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
如果你在实现某个功能时发现以上规则不够具体(例如某类任务在 `core/tasks` 中到底如何落地,或某个存储/解析器的边界不清晰),请在对应章节下扩展更精确的说明,并在 PR 描述中标注。也欢迎你告诉我有哪些部分需要补充或澄清,我可以进一步细化。
|
||||||
@@ -15,7 +15,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
|||||||
--mount=type=cache,target=/go/pkg \
|
--mount=type=cache,target=/go/pkg \
|
||||||
CGO_ENABLED=0 \
|
CGO_ENABLED=0 \
|
||||||
go build -trimpath \
|
go build -trimpath \
|
||||||
-tags=no_jsparser,no_minio \
|
-tags=no_jsparser,no_minio,no_bubbletea \
|
||||||
-ldflags=" \
|
-ldflags=" \
|
||||||
-s -w \
|
-s -w \
|
||||||
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
|||||||
--mount=type=cache,target=/go/pkg \
|
--mount=type=cache,target=/go/pkg \
|
||||||
CGO_ENABLED=0 \
|
CGO_ENABLED=0 \
|
||||||
go build -trimpath \
|
go build -trimpath \
|
||||||
-tags=no_jsparser,no_minio,sqlite_glebarez \
|
-tags=no_jsparser,no_minio,sqlite_glebarez,no_bubbletea \
|
||||||
-ldflags=" \
|
-ldflags=" \
|
||||||
-s -w \
|
-s -w \
|
||||||
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
-X 'github.com/krau/SaveAny-Bot/config.Version=${VERSION}' \
|
||||||
|
|||||||
96
README.md
96
README.md
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
# <img src="docs/static/logo.png" width="45" align="center"> Save Any Bot
|
# <img src="docs/static/logo.png" width="45" align="center"> Save Any Bot
|
||||||
|
|
||||||
**简体中文** | [English](https://sabot.unv.app/en/)
|
**English** | [简体中文](./README_zh.md)
|
||||||
|
|
||||||
> **把 Telegram 上的文件转存到多种存储端.**
|
> **Save Any Telegram File to Anywhere 📂. Support restrict saving content and beyond telegram.**
|
||||||
|
|
||||||
[](https://github.com/krau/saveany-bot/releases)
|
[](https://github.com/krau/saveany-bot/releases)
|
||||||
[](https://github.com/krau/saveany-bot/releases)
|
[](https://github.com/krau/saveany-bot/releases)
|
||||||
@@ -19,46 +19,46 @@
|
|||||||
|
|
||||||
## 🎯 Features
|
## 🎯 Features
|
||||||
|
|
||||||
- 支持文档/视频/图片/贴纸…甚至还有 [Telegraph](https://telegra.ph/)
|
- Support documents / videos / photos / stickers… and even [Telegraph](https://telegra.ph/)
|
||||||
- 破解禁止保存的文件
|
- Bypass "restrict saving content" media
|
||||||
- 批量下载
|
- Batch download
|
||||||
- 流式传输
|
- Streaming transfer
|
||||||
- 多用户使用
|
- Multi-user support
|
||||||
- 基于存储规则的自动整理
|
- Auto organize files based on storage rules
|
||||||
- 监听并自动转存指定聊天的消息, 支持过滤
|
- Watch specified chats and auto-save messages, with filters
|
||||||
- 使用 js 编写解析器插件以转存任意网站的文件
|
- Write JS parser plugins to save files from almost any website
|
||||||
- 存储端支持:
|
- Storage backends:
|
||||||
- Alist
|
- Alist
|
||||||
- S3
|
- S3
|
||||||
- WebDAV
|
- WebDAV
|
||||||
- 本地磁盘
|
- Local filesystem
|
||||||
- Telegram (重传回指定聊天)
|
- Telegram (re-upload to specified chats)
|
||||||
|
|
||||||
## 📦 Quick Start
|
## 📦 Quick Start
|
||||||
|
|
||||||
创建文件 `config.toml` 并填入以下内容:
|
Create a `config.toml` file with the following content:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[telegram]
|
[telegram]
|
||||||
token = "" # 你的 Bot Token, 在 @BotFather 获取
|
token = "" # Your bot token, obtained from @BotFather
|
||||||
[telegram.proxy]
|
[telegram.proxy]
|
||||||
# 启用代理连接 telegram, 当前只支持 socks5
|
# Enable proxy for Telegram, currently only SOCKS5 is supported
|
||||||
enable = false
|
enable = false
|
||||||
url = "socks5://127.0.0.1:7890"
|
url = "socks5://127.0.0.1:7890"
|
||||||
|
|
||||||
[[storages]]
|
[[storages]]
|
||||||
name = "本地磁盘"
|
name = "Local Disk"
|
||||||
type = "local"
|
type = "local"
|
||||||
enable = true
|
enable = true
|
||||||
base_path = "./downloads"
|
base_path = "./downloads"
|
||||||
|
|
||||||
[[users]]
|
[[users]]
|
||||||
id = 114514 # 你的 Telegram 账号 id
|
id = 114514 # Your Telegram account id
|
||||||
storages = []
|
storages = []
|
||||||
blacklist = true
|
blacklist = true
|
||||||
```
|
```
|
||||||
|
|
||||||
使用 Docker 运行 Save Any Bot:
|
Run Save Any Bot with Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d --name saveany-bot \
|
docker run -d --name saveany-bot \
|
||||||
@@ -67,69 +67,23 @@ docker run -d --name saveany-bot \
|
|||||||
ghcr.io/krau/saveany-bot:latest
|
ghcr.io/krau/saveany-bot:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
请 [**查看文档**](https://sabot.unv.app/) 以获取更多配置选项和使用方法.
|
Please [**read the docs**](https://sabot.unv.app/en/) for more configuration options and usage.
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
本项目受到 [YxVM](https://yxvm.com/) 与 [NodeSupport](https://github.com/NodeSeekDev/NodeSupport) 的支持.
|
This project is supported by [YxVM](https://yxvm.com/) and [NodeSupport](https://github.com/NodeSeekDev/NodeSupport).
|
||||||
|
|
||||||
如果这个项目对你有帮助, 你可以考虑通过以下方式赞助我:
|
If this project is helpful to you, consider sponsoring me via:
|
||||||
|
|
||||||
- [爱发电](https://afdian.com/a/unvapp)
|
- [Afdian](https://afdian.com/a/unvapp)
|
||||||
|
|
||||||
## Contributors
|
## Thanks To
|
||||||
|
|
||||||
<!-- readme: contributors -start -->
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/krau">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/71133316?v=4" width="100;" alt="krau"/>
|
|
||||||
<br />
|
|
||||||
<sub><b>Krau</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/Silentely">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/22141172?v=4" width="100;" alt="Silentely"/>
|
|
||||||
<br />
|
|
||||||
<sub><b>Abner</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/TG-Twilight">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/121682528?v=4" width="100;" alt="TG-Twilight"/>
|
|
||||||
<br />
|
|
||||||
<sub><b>Simon Twilight</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/ysicing">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/8605565?v=4" width="100;" alt="ysicing"/>
|
|
||||||
<br />
|
|
||||||
<sub><b>缘生</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td align="center">
|
|
||||||
<a href="https://github.com/AHCorn">
|
|
||||||
<img src="https://avatars.githubusercontent.com/u/42889600?v=4" width="100;" alt="AHCorn"/>
|
|
||||||
<br />
|
|
||||||
<sub><b>安和</b></sub>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tbody>
|
|
||||||
</table>
|
|
||||||
<!-- readme: contributors -end -->
|
|
||||||
|
|
||||||
## Thanks
|
|
||||||
|
|
||||||
- [gotd](https://github.com/gotd/td)
|
- [gotd](https://github.com/gotd/td)
|
||||||
- [TG-FileStreamBot](https://github.com/EverythingSuckz/TG-FileStreamBot)
|
- [TG-FileStreamBot](https://github.com/EverythingSuckz/TG-FileStreamBot)
|
||||||
- [gotgproto](https://github.com/celestix/gotgproto)
|
- [gotgproto](https://github.com/celestix/gotgproto)
|
||||||
- [tdl](https://github.com/iyear/tdl)
|
- [tdl](https://github.com/iyear/tdl)
|
||||||
- All the dependencies
|
- All the dependencies, contributors, sponsors and users.
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
|
|||||||
90
README_zh.md
Normal file
90
README_zh.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# <img src="docs/static/logo.png" width="45" align="center"> Save Any Bot
|
||||||
|
|
||||||
|
> **把 Telegram 上的文件转存到多种存储端**
|
||||||
|
|
||||||
|
[](https://github.com/krau/saveany-bot/releases)
|
||||||
|
[](https://github.com/krau/saveany-bot/releases)
|
||||||
|
[](https://github.com/krau/saveany-bot/actions/workflows/build-release.yml)
|
||||||
|
[](https://github.com/krau/saveany-bot/stargazers)
|
||||||
|
[](https://github.com/krau/saveany-bot/releases)
|
||||||
|
[](https://github.com/krau/saveany-bot/issues)
|
||||||
|
[](https://github.com/krau/saveany-bot/pulls)
|
||||||
|
[](./LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 🎯 特性
|
||||||
|
|
||||||
|
- 支持文档/视频/图片/贴纸…甚至还有 [Telegraph](https://telegra.ph/)
|
||||||
|
- 破解禁止保存的文件
|
||||||
|
- 批量下载
|
||||||
|
- 流式传输
|
||||||
|
- 多用户使用
|
||||||
|
- 基于存储规则的自动整理
|
||||||
|
- 监听并自动转存指定聊天的消息, 支持过滤
|
||||||
|
- 使用 js 编写解析器插件以转存任意网站的文件
|
||||||
|
- 存储端支持:
|
||||||
|
- Alist
|
||||||
|
- S3
|
||||||
|
- WebDAV
|
||||||
|
- 本地磁盘
|
||||||
|
- Telegram (重传回指定聊天)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
创建文件 `config.toml` 并填入以下内容:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[telegram]
|
||||||
|
token = "" # 你的 Bot Token, 在 @BotFather 获取
|
||||||
|
[telegram.proxy]
|
||||||
|
# 启用代理连接 telegram, 当前只支持 socks5
|
||||||
|
enable = false
|
||||||
|
url = "socks5://127.0.0.1:7890"
|
||||||
|
|
||||||
|
[[storages]]
|
||||||
|
name = "本地磁盘"
|
||||||
|
type = "local"
|
||||||
|
enable = true
|
||||||
|
base_path = "./downloads"
|
||||||
|
|
||||||
|
[[users]]
|
||||||
|
id = 114514 # 你的 Telegram 账号 id
|
||||||
|
storages = []
|
||||||
|
blacklist = true
|
||||||
|
```
|
||||||
|
|
||||||
|
使用 Docker 运行 Save Any Bot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name saveany-bot \
|
||||||
|
-v ./config.toml:/app/config.toml \
|
||||||
|
-v ./downloads:/app/downloads \
|
||||||
|
ghcr.io/krau/saveany-bot:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
请 [**查看文档**](https://sabot.unv.app/) 以获取更多配置选项和使用方法.
|
||||||
|
|
||||||
|
## 赞助
|
||||||
|
|
||||||
|
本项目受到 [YxVM](https://yxvm.com/) 与 [NodeSupport](https://github.com/NodeSeekDev/NodeSupport) 的支持.
|
||||||
|
|
||||||
|
如果这个项目对你有帮助, 你可以考虑通过以下方式赞助我:
|
||||||
|
|
||||||
|
- [爱发电](https://afdian.com/a/unvapp)
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
- [gotd](https://github.com/gotd/td)
|
||||||
|
- [TG-FileStreamBot](https://github.com/EverythingSuckz/TG-FileStreamBot)
|
||||||
|
- [gotgproto](https://github.com/celestix/gotgproto)
|
||||||
|
- [tdl](https://github.com/iyear/tdl)
|
||||||
|
- All the dependencies, contributors, sponsors and users.
|
||||||
|
|
||||||
|
## 社区和关于作者
|
||||||
|
|
||||||
|
- [](https://t.me/ProjectSaveAny)
|
||||||
|
- [](https://github.com/krau/saveany-bot/discussions)
|
||||||
|
- [](https://t.me/acherkrau)
|
||||||
@@ -12,13 +12,20 @@ import (
|
|||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers"
|
||||||
"github.com/krau/SaveAny-Bot/client/middleware"
|
"github.com/krau/SaveAny-Bot/client/middleware"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ectx *ext.Context
|
||||||
|
|
||||||
|
func ExtContext() *ext.Context {
|
||||||
|
return ectx
|
||||||
|
}
|
||||||
|
|
||||||
func Init(ctx context.Context) <-chan struct{} {
|
func Init(ctx context.Context) <-chan struct{} {
|
||||||
log.FromContext(ctx).Info("初始化 Bot...")
|
log.FromContext(ctx).Info("Initializing Bot...")
|
||||||
resultChan := make(chan struct {
|
resultChan := make(chan struct {
|
||||||
client *gotgproto.Client
|
client *gotgproto.Client
|
||||||
err error
|
err error
|
||||||
@@ -68,7 +75,7 @@ func Init(ctx context.Context) <-chan struct{} {
|
|||||||
})
|
})
|
||||||
commands := make([]tg.BotCommand, 0, len(handlers.CommandHandlers))
|
commands := make([]tg.BotCommand, 0, len(handlers.CommandHandlers))
|
||||||
for _, info := range handlers.CommandHandlers {
|
for _, info := range handlers.CommandHandlers {
|
||||||
commands = append(commands, tg.BotCommand{Command: info.Cmd, Description: info.Desc})
|
commands = append(commands, tg.BotCommand{Command: info.Cmd, Description: i18n.T(info.Desc)})
|
||||||
}
|
}
|
||||||
_, err = client.API().BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{
|
_, err = client.API().BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{
|
||||||
Scope: &tg.BotCommandScopeDefault{},
|
Scope: &tg.BotCommandScopeDefault{},
|
||||||
@@ -82,13 +89,14 @@ func Init(ctx context.Context) <-chan struct{} {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.FromContext(ctx).Errorf("已取消 Bot 初始化: %s", ctx.Err())
|
log.FromContext(ctx).Errorf("Bot initialization cancelled: %s", ctx.Err())
|
||||||
case result := <-resultChan:
|
case result := <-resultChan:
|
||||||
if result.err != nil {
|
if result.err != nil {
|
||||||
log.FromContext(ctx).Fatalf("初始化 Bot 失败: %s", result.err)
|
log.FromContext(ctx).Fatalf("Failed to initialize Bot: %s", result.err)
|
||||||
}
|
}
|
||||||
handlers.Register(result.client.Dispatcher)
|
handlers.Register(result.client.Dispatcher)
|
||||||
log.FromContext(ctx).Info("Bot 初始化完成")
|
ectx = result.client.CreateContext()
|
||||||
|
log.FromContext(ctx).Info("Bot initialization completed.")
|
||||||
}
|
}
|
||||||
return shouldRestart
|
return shouldRestart
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
@@ -33,12 +35,14 @@ func handleAddCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
selectedStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, data.SelectedStorName)
|
selectedStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, data.SelectedStorName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to get storage: %s", err)
|
log.FromContext(ctx).Errorf("Failed to get storage: %s", err)
|
||||||
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, "存储获取失败: "+err.Error()))
|
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, i18n.T(i18nk.BotMsgCommonErrorGetStorageFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})))
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
dirs, err := database.GetDirsByUserChatIDAndStorageName(ctx, userID, data.SelectedStorName)
|
dirs, err := database.GetDirsByUserChatIDAndStorageName(ctx, userID, data.SelectedStorName)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fmt.Errorf("获取用户目录失败: %w", err)
|
return fmt.Errorf("failed to get user directories: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.SettedDir && len(dirs) != 0 {
|
if !data.SettedDir && len(dirs) != 0 {
|
||||||
@@ -46,12 +50,14 @@ func handleAddCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
markup, err := msgelem.BuildSetDirMarkupForAdd(dirs, dataid)
|
markup, err := msgelem.BuildSetDirMarkupForAdd(dirs, dataid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build directory keyboard: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build directory keyboard: %s", err)
|
||||||
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, "目录键盘构建失败: "+err.Error()))
|
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, i18n.T(i18nk.BotMsgCommonErrorBuildStorageSelectKeyboardFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})))
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: update.CallbackQuery.GetMsgID(),
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
Message: "请选择要存储到的目录",
|
Message: i18n.T(i18nk.BotMsgCommonPromptSelectDir, nil),
|
||||||
ReplyMarkup: markup,
|
ReplyMarkup: markup,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
@@ -61,7 +67,9 @@ func handleAddCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if data.DirID != 0 {
|
if data.DirID != 0 {
|
||||||
dir, err := database.GetDirByID(ctx, data.DirID)
|
dir, err := database.GetDirByID(ctx, data.DirID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, "获取目录失败: "+err.Error()))
|
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, i18n.T(i18nk.BotMsgCommonErrorGetDirFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})))
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
dirPath = dir.Path
|
dirPath = dir.Path
|
||||||
|
|||||||
@@ -8,20 +8,24 @@ import (
|
|||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/core"
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleCancelCallback(ctx *ext.Context, update *ext.Update) error {
|
func handleCancelCallback(ctx *ext.Context, update *ext.Update) error {
|
||||||
taskid := strings.Split(string(update.CallbackQuery.Data), " ")[1]
|
taskid := strings.Split(string(update.CallbackQuery.Data), " ")[1]
|
||||||
if err := core.CancelTask(ctx, taskid); err != nil {
|
if err := core.CancelTask(ctx, taskid); err != nil {
|
||||||
log.FromContext(ctx).Errorf("error cancelling task %s: %v", taskid, err)
|
log.FromContext(ctx).Errorf("Failed to cancel task %s: %v", taskid, err)
|
||||||
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(update.CallbackQuery.GetQueryID(), "取消任务失败: "+err.Error()))
|
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(update.CallbackQuery.GetQueryID(), i18n.T(i18nk.BotMsgCancelErrorCancelFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})))
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.EditMessage(update.CallbackQuery.GetUserID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.CallbackQuery.GetUserID(), &tg.MessagesEditMessageRequest{
|
||||||
ID: update.CallbackQuery.GetMsgID(),
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
Message: "正在取消任务...",
|
Message: i18n.T(i18nk.BotMsgCancelInfoCancellingTask, nil),
|
||||||
})
|
})
|
||||||
|
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
@@ -31,15 +35,19 @@ func handleCancelCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
args := strings.Fields(update.EffectiveMessage.Text)
|
args := strings.Fields(update.EffectiveMessage.Text)
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("用法: /cancel <task_id>"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCancelUsage, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
taskID := args[1]
|
taskID := args[1]
|
||||||
if err := core.CancelTask(ctx, taskID); err != nil {
|
if err := core.CancelTask(ctx, taskID); err != nil {
|
||||||
logger.Errorf("failed to cancel task %s: %v", taskID, err)
|
logger.Errorf("failed to cancel task %s: %v", taskID, err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("取消任务失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCancelErrorCancelFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString("已请求取消任务: "+taskID), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCancelInfoCancelRequested, map[string]any{
|
||||||
|
"TaskID": taskID,
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,19 +8,21 @@ import (
|
|||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/fnamest"
|
"github.com/krau/SaveAny-Bot/pkg/enums/fnamest"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleConfigCmd(ctx *ext.Context, update *ext.Update) error {
|
func handleConfigCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
ctx.Reply(update, ext.ReplyTextString("请选择要配置的选项"), &ext.ReplyOpts{
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgConfigPromptSelectOption)), &ext.ReplyOpts{
|
||||||
Markup: &tg.ReplyInlineMarkup{
|
Markup: &tg.ReplyInlineMarkup{
|
||||||
Rows: []tg.KeyboardButtonRow{
|
Rows: []tg.KeyboardButtonRow{
|
||||||
{
|
{
|
||||||
Buttons: []tg.KeyboardButtonClass{
|
Buttons: []tg.KeyboardButtonClass{
|
||||||
&tg.KeyboardButtonCallback{
|
&tg.KeyboardButtonCallback{
|
||||||
Text: "文件名策略",
|
Text: i18n.T(i18nk.BotMsgConfigButtonFilenameStrategy),
|
||||||
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeConfig, "fnamest"),
|
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeConfig, "fnamest"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -37,7 +39,7 @@ func handleConfigCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
|
||||||
QueryID: update.CallbackQuery.GetQueryID(),
|
QueryID: update.CallbackQuery.GetQueryID(),
|
||||||
Alert: true,
|
Alert: true,
|
||||||
Message: "无效的回调数据",
|
Message: i18n.T(i18nk.BotMsgConfigErrorInvalidCallbackData),
|
||||||
CacheTime: 5,
|
CacheTime: 5,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
@@ -72,7 +74,9 @@ func handleConfigFnameSTCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: update.CallbackQuery.GetMsgID(),
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
Message: fmt.Sprintf("已将文件名策略设置为: %s", fnamest.FnameSTDisplay[st]),
|
Message: i18n.T(i18nk.BotMsgConfigInfoFilenameStrategySet, map[string]any{
|
||||||
|
"Strategy": fnamest.FnameSTDisplay[st],
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -97,7 +101,9 @@ func handleConfigFnameSTCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: update.CallbackQuery.GetMsgID(),
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
Message: fmt.Sprintf("请选择文件名策略, 当前策略: %s", fnamest.FnameSTDisplay[currentSt]),
|
Message: i18n.T(i18nk.BotMsgConfigPromptSelectFilenameStrategy, map[string]any{
|
||||||
|
"Strategy": fnamest.FnameSTDisplay[currentSt],
|
||||||
|
}),
|
||||||
ReplyMarkup: markup,
|
ReplyMarkup: markup,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
@@ -111,34 +117,27 @@ func handleConfigFnameTmpl(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
args := strings.Fields(string(update.EffectiveMessage.Text))
|
args := strings.Fields(string(update.EffectiveMessage.Text))
|
||||||
if len(args) <= 1 {
|
if len(args) <= 1 {
|
||||||
text := `使用该命令设置文件名模板, 示例:
|
text := i18n.T(i18nk.BotMsgConfigFnametmplHelp, nil)
|
||||||
/fnametmpl 图片_{{.msgid}}_{{.msgdate}}.jpg
|
|
||||||
|
|
||||||
可用变量:
|
|
||||||
- {{.msgid}}: 消息ID
|
|
||||||
- {{.msgtags}}: 消息中的标签, 将以下划线分隔输出
|
|
||||||
- {{.msggen}}: 根据消息生成的文件名
|
|
||||||
- {{.msgdate}}: 消息日期, 格式 YYYY-MM-DD_HH-MM-SS
|
|
||||||
- {{.origname}}: 媒体的原始文件名 (如果有)
|
|
||||||
- {{.chatid}}: 消息的聊天ID
|
|
||||||
`
|
|
||||||
if user.FilenameTemplate != "" {
|
if user.FilenameTemplate != "" {
|
||||||
text += fmt.Sprintf("\n\n当前模板: %s", user.FilenameTemplate)
|
text += "\n\n" + i18n.T(i18nk.BotMsgConfigInfoCurrentTemplatePrefix, map[string]any{
|
||||||
|
"Template": user.FilenameTemplate,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
text += "\n\n模板仅在文件名策略设置为 '自定义模板' 时生效, 且模板解析错误时会回退到默认文件名"
|
|
||||||
ctx.Reply(update, ext.ReplyTextString(text), nil)
|
ctx.Reply(update, ext.ReplyTextString(text), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
newTmpl := strings.Join(args[1:], " ")
|
newTmpl := strings.Join(args[1:], " ")
|
||||||
_, err = template.New("filename").Parse(newTmpl)
|
_, err = template.New("filename").Parse(newTmpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("无效的模板, 请检查语法\n"+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgConfigErrorInvalidTemplate, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
user.FilenameTemplate = newTmpl
|
user.FilenameTemplate = newTmpl
|
||||||
if err := database.UpdateUser(ctx, user); err != nil {
|
if err := database.UpdateUser(ctx, user); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString("已更新文件名模板"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgConfigInfoTemplateUpdated, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
)
|
)
|
||||||
@@ -18,8 +20,8 @@ func handleDirCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
userChatID := update.GetUserChat().GetID()
|
userChatID := update.GetUserChat().GetID()
|
||||||
dirs, err := database.GetUserDirsByChatID(ctx, userChatID)
|
dirs, err := database.GetUserDirsByChatID(ctx, userChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("获取用户文件夹失败: %s", err)
|
logger.Errorf("Failed to get user directories: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取用户文件夹失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDirErrorGetUserDirsFailed)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
@@ -28,8 +30,8 @@ func handleDirCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
user, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
user, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("获取用户失败: %s", err)
|
logger.Errorf("Failed to get user: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDirErrorGetUserFailed)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
switch args[1] {
|
switch args[1] {
|
||||||
@@ -45,11 +47,11 @@ func handleDirCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := database.CreateDirForUser(ctx, user.ID, args[2], args[3]); err != nil {
|
if err := database.CreateDirForUser(ctx, user.ID, args[2], args[3]); err != nil {
|
||||||
logger.Errorf("创建文件夹失败: %s", err)
|
logger.Errorf("Failed to create directory: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("创建文件夹失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDirErrorCreateDirFailed)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString("文件夹添加成功"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDirInfoCreateDirSuccess)), nil)
|
||||||
case "del":
|
case "del":
|
||||||
// /dir del 3
|
// /dir del 3
|
||||||
if len(args) < 3 {
|
if len(args) < 3 {
|
||||||
@@ -58,17 +60,17 @@ func handleDirCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
dirID, err := strconv.Atoi(args[2])
|
dirID, err := strconv.Atoi(args[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("文件夹ID无效"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDirErrorInvalidDirId)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if err := database.DeleteDirByID(ctx, uint(dirID)); err != nil {
|
if err := database.DeleteDirByID(ctx, uint(dirID)); err != nil {
|
||||||
logger.Errorf("删除文件夹失败: %s", err)
|
logger.Errorf("Failed to delete directory: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("删除文件夹失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDirErrorDeleteDirFailed)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString("文件夹删除成功"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDirInfoDeleteDirSuccess)), nil)
|
||||||
default:
|
default:
|
||||||
ctx.Reply(update, ext.ReplyTextString("未知操作"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDirErrorUnknownOperation)), nil)
|
||||||
}
|
}
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -9,6 +8,8 @@ import (
|
|||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
@@ -18,7 +19,7 @@ func handleDlCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
args := strings.Split(update.EffectiveMessage.Text, " ")
|
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("用法: /dl <链接1> <链接2> ..."), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDlUsage)), nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
links := args[1:]
|
links := args[1:]
|
||||||
@@ -32,7 +33,7 @@ func handleDlCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
links = slice.Compact(links)
|
links = slice.Compact(links)
|
||||||
if len(links) == 0 {
|
if len(links) == 0 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("没有有效的链接可供下载"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDlErrorNoValidLinks)), nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
markup, err := msgelem.BuildAddSelectStorageKeyboard(storage.GetUserStorages(ctx, update.GetUserChat().GetID()), tcbdata.Add{
|
markup, err := msgelem.BuildAddSelectStorageKeyboard(storage.GetUserStorages(ctx, update.GetUserChat().GetID()), tcbdata.Add{
|
||||||
@@ -42,7 +43,9 @@ func handleDlCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("共 %d 个文件, 请选择存储位置", len(links))), &ext.ReplyOpts{
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgDlInfoFilesSelectStorage, map[string]any{
|
||||||
|
"Count": len(links),
|
||||||
|
})), &ext.ReplyOpts{
|
||||||
Markup: markup,
|
Markup: markup,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
)
|
)
|
||||||
@@ -24,8 +24,10 @@ func handleMessageLink(ctx *ext.Context, update *ext.Update) error {
|
|||||||
if len(files) == 1 {
|
if len(files) == 1 {
|
||||||
req, err := msgelem.BuildAddOneSelectStorageMessage(ctx, stors, files[0], replied.ID)
|
req, err := msgelem.BuildAddOneSelectStorageMessage(ctx, stors, files[0], replied.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("构建存储选择消息失败: %s", err)
|
logger.Errorf("Failed to build storage selection message: %s", err)
|
||||||
editReplied("构建存储选择消息失败: "+err.Error(), nil)
|
editReplied(i18n.T(i18nk.BotMsgCommonErrorBuildStorageSelectMessageFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), req)
|
ctx.EditMessage(update.EffectiveChat().GetID(), req)
|
||||||
@@ -35,11 +37,15 @@ func handleMessageLink(ctx *ext.Context, update *ext.Update) error {
|
|||||||
Files: files,
|
Files: files,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("构建存储选择键盘失败: %s", err)
|
logger.Errorf("Failed to build storage selection keyboard: %s", err)
|
||||||
editReplied("构建存储选择键盘失败: "+err.Error(), nil)
|
editReplied(i18n.T(i18nk.BotMsgCommonErrorBuildStorageSelectKeyboardFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
editReplied(fmt.Sprintf("找到 %d 个文件, 请选择存储位置", len(files)), markup)
|
editReplied(i18n.T(i18nk.BotMsgCommonInfoFoundFilesSelectStorage, map[string]any{
|
||||||
|
"Count": len(files),
|
||||||
|
}), markup)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
)
|
)
|
||||||
@@ -34,8 +36,10 @@ func handleMediaMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
stors := storage.GetUserStorages(ctx, userId)
|
stors := storage.GetUserStorages(ctx, userId)
|
||||||
req, err := msgelem.BuildAddOneSelectStorageMessage(ctx, stors, file, msg.ID)
|
req, err := msgelem.BuildAddOneSelectStorageMessage(ctx, stors, file, msg.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("构建存储选择消息失败: %s", err)
|
logger.Errorf("Failed to build storage selection message: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("构建存储选择消息失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorBuildStorageSelectMessageFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), req)
|
ctx.EditMessage(update.EffectiveChat().GetID(), req)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,6 +11,8 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
@@ -89,7 +90,7 @@ func processMediaGroup(ctx *ext.Context, update *ext.Update, groupID int64) {
|
|||||||
logger.Debugf("Processing media group %d with %d items", groupID, len(items))
|
logger.Debugf("Processing media group %d with %d items", groupID, len(items))
|
||||||
|
|
||||||
userId := update.GetUserChat().GetID()
|
userId := update.GetUserChat().GetID()
|
||||||
msg, err := ctx.Reply(update, ext.ReplyTextString("正在保存文件..."), nil)
|
msg, err := ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgMediaGroupInfoSavingFiles, nil)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to reply: %s", err)
|
logger.Errorf("Failed to reply: %s", err)
|
||||||
return
|
return
|
||||||
@@ -111,16 +112,20 @@ func processMediaGroup(ctx *ext.Context, update *ext.Update, groupID int64) {
|
|||||||
AsBatch: len(items) > 1,
|
AsBatch: len(items) > 1,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("构建存储选择键盘失败: %s", err)
|
logger.Errorf("Failed to build storage selection keyboard: %s", err)
|
||||||
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
||||||
ID: msg.ID,
|
ID: msg.ID,
|
||||||
Message: "构建存储选择键盘失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgMediaGroupErrorBuildStorageSelectKeyboardFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userId, &tg.MessagesEditMessageRequest{
|
||||||
ID: msg.ID,
|
ID: msg.ID,
|
||||||
Message: fmt.Sprintf("共 %d 个文件, 请选择存储位置", len(items)),
|
Message: i18n.T(i18nk.BotMsgMediaGroupInfoGroupFoundFilesSelectStorage, map[string]any{
|
||||||
|
"Count": len(items),
|
||||||
|
}),
|
||||||
ReplyMarkup: markup,
|
ReplyMarkup: markup,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
@@ -13,11 +15,7 @@ import (
|
|||||||
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
||||||
userID := update.GetUserChat().GetID()
|
userID := update.GetUserChat().GetID()
|
||||||
if !slice.Contain(config.C().GetUsersID(), userID) {
|
if !slice.Contain(config.C().GetUsersID(), userID) {
|
||||||
const noPermissionText string = `
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorNoPermission, nil)), nil)
|
||||||
您不在白名单中, 无法使用此 Bot.
|
|
||||||
您可以部署自己的实例: https://github.com/krau/SaveAny-Bot
|
|
||||||
`
|
|
||||||
ctx.Reply(update, ext.ReplyTextString(noPermissionText), nil)
|
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,25 +27,31 @@ func handleSilentMode(next func(*ext.Context, *ext.Update) error, handler func(*
|
|||||||
userID := update.GetUserChat().GetID()
|
userID := update.GetUserChat().GetID()
|
||||||
user, err := database.GetUserByChatID(ctx, userID)
|
user, err := database.GetUserByChatID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取用户信息失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetUserInfoFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if !user.Silent {
|
if !user.Silent {
|
||||||
return next(ctx, update)
|
return next(ctx, update)
|
||||||
}
|
}
|
||||||
if user.DefaultStorage == "" {
|
if user.DefaultStorage == "" {
|
||||||
ctx.Reply(update, ext.ReplyTextString("您已开启静默模式, 但未设置默认存储端, 请先使用 /storage 设置"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorDefaultStorageNotSet, nil)), nil)
|
||||||
return next(ctx, update)
|
return next(ctx, update)
|
||||||
}
|
}
|
||||||
stor, err := storage.GetStorageByUserIDAndName(ctx, userID, user.DefaultStorage)
|
stor, err := storage.GetStorageByUserIDAndName(ctx, userID, user.DefaultStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取默认存储失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetStorageFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if user.DefaultDir != 0 {
|
if user.DefaultDir != 0 {
|
||||||
dir, err := database.GetDirByID(ctx, user.DefaultDir)
|
dir, err := database.GetDirByID(ctx, user.DefaultDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取默认文件夹失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetDirFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return next(ctx, update)
|
return next(ctx, update)
|
||||||
}
|
}
|
||||||
ctx.Context = dirutil.WithContext(ctx.Context, dir)
|
ctx.Context = dirutil.WithContext(ctx.Context, dir)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/parsers"
|
"github.com/krau/SaveAny-Bot/parsers"
|
||||||
@@ -33,7 +35,7 @@ func handleTextMessage(ctx *ext.Context, u *ext.Update) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
msg, err := ctx.Reply(u, ext.ReplyTextString("正在解析..."), nil)
|
msg, err := ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParseInfoParsing, nil)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -44,7 +46,9 @@ func handleTextMessage(ctx *ext.Context, u *ext.Update) error {
|
|||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to parse text", "error", err)
|
logger.Error("Failed to parse text", "error", err)
|
||||||
ctx.Reply(u, ext.ReplyTextString("Failed to parse text: "+err.Error()), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParseErrorParseTextFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
logger.Debug("Parsed item from text message", "title", item.Title, "url", item.URL)
|
logger.Debug("Parsed item from text message", "title", item.Title, "url", item.URL)
|
||||||
@@ -55,13 +59,17 @@ func handleTextMessage(ctx *ext.Context, u *ext.Update) error {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to build storage selection keyboard: %s", err)
|
logger.Errorf("Failed to build storage selection keyboard: %s", err)
|
||||||
ctx.Reply(u, ext.ReplyTextString("Failed to build storage selection keyboard: "+err.Error()), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParseErrorBuildStorageSelectKeyboardFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
text, entities, err := msgelem.BuildParsedTextEntity(*item)
|
text, entities, err := msgelem.BuildParsedTextEntity(*item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to build parsed text entity: %s", err)
|
logger.Errorf("Failed to build parsed text entity: %s", err)
|
||||||
ctx.Reply(u, ext.ReplyTextString("Failed to build parsed text entity: "+err.Error()), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParseErrorBuildParsedTextEntityFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
@@ -87,7 +95,9 @@ func handleSilentSaveText(ctx *ext.Context, u *ext.Update) error {
|
|||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to parse text", "error", err)
|
logger.Error("Failed to parse text", "error", err)
|
||||||
ctx.Reply(u, ext.ReplyTextString("Failed to parse text: "+err.Error()), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParseErrorParseTextFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
logger.Debug("Parsed item from text message", "title", item.Title, "url", item.URL)
|
logger.Debug("Parsed item from text message", "title", item.Title, "url", item.URL)
|
||||||
@@ -95,7 +105,9 @@ func handleSilentSaveText(ctx *ext.Context, u *ext.Update) error {
|
|||||||
text, entities, err := msgelem.BuildParsedTextEntity(*item)
|
text, entities, err := msgelem.BuildParsedTextEntity(*item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to build parsed text entity: %s", err)
|
logger.Errorf("Failed to build parsed text entity: %s", err)
|
||||||
ctx.Reply(u, ext.ReplyTextString("Failed to build parsed text entity: "+err.Error()), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParseErrorBuildParsedTextEntityFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
msg, err := ctx.SendMessage(userID, &tg.MessagesSendMessageRequest{
|
msg, err := ctx.SendMessage(userID, &tg.MessagesSendMessageRequest{
|
||||||
|
|||||||
@@ -7,17 +7,15 @@ import (
|
|||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/parsers"
|
"github.com/krau/SaveAny-Bot/parsers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleParserCmd(ctx *ext.Context, u *ext.Update) error {
|
func handleParserCmd(ctx *ext.Context, u *ext.Update) error {
|
||||||
args := strings.Split(u.EffectiveMessage.Text, " ")
|
args := strings.Split(u.EffectiveMessage.Text, " ")
|
||||||
help := `
|
help := i18n.T(i18nk.BotMsgParserHelpText, nil)
|
||||||
用法:
|
|
||||||
|
|
||||||
/parser install <回复一个文件> - 安装解析器
|
|
||||||
`
|
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
ctx.Reply(u, ext.ReplyTextString(help), nil)
|
ctx.Reply(u, ext.ReplyTextString(help), nil)
|
||||||
return nil
|
return nil
|
||||||
@@ -36,35 +34,35 @@ func handleParserCmd(ctx *ext.Context, u *ext.Update) error {
|
|||||||
|
|
||||||
func handleParserInstallCmd(ctx *ext.Context, u *ext.Update) error {
|
func handleParserInstallCmd(ctx *ext.Context, u *ext.Update) error {
|
||||||
if !config.C().Parser.PluginEnable {
|
if !config.C().Parser.PluginEnable {
|
||||||
ctx.Reply(u, ext.ReplyTextString("解析器插件功能未启用"), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserPluginNotEnabled, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if u.EffectiveMessage.ReplyToMessage == nil || u.EffectiveMessage.ReplyToMessage.Media == nil {
|
if u.EffectiveMessage.ReplyToMessage == nil || u.EffectiveMessage.ReplyToMessage.Media == nil {
|
||||||
ctx.Reply(u, ext.ReplyTextString("请回复一个包含解析器文件的消息"), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserPromptReplyWithParserFile, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
media := u.EffectiveMessage.ReplyToMessage.Media
|
media := u.EffectiveMessage.ReplyToMessage.Media
|
||||||
document, ok := media.(*tg.MessageMediaDocument)
|
document, ok := media.(*tg.MessageMediaDocument)
|
||||||
if !ok {
|
if !ok {
|
||||||
ctx.Reply(u, ext.ReplyTextString("回复的消息不包含有效的文件"), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserErrorNoValidFileInReply, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
value, ok := document.GetDocument()
|
value, ok := document.GetDocument()
|
||||||
if !ok {
|
if !ok {
|
||||||
ctx.Reply(u, ext.ReplyTextString("回复的消息不包含有效的文件"), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserErrorNoValidFileInReply, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
doc, ok := value.AsNotEmpty()
|
doc, ok := value.AsNotEmpty()
|
||||||
if !ok {
|
if !ok {
|
||||||
ctx.Reply(u, ext.ReplyTextString("回复的消息不包含有效的文件"), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserErrorNoValidFileInReply, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(doc.MimeType, "text/") {
|
if !strings.HasPrefix(doc.MimeType, "text/") {
|
||||||
ctx.Reply(u, ext.ReplyTextString("错误的文件类型"), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserErrorWrongFileType, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if doc.Size > 1024*1024*10 {
|
if doc.Size > 1024*1024*10 {
|
||||||
ctx.Reply(u, ext.ReplyTextString("文件过大"), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserErrorFileTooLarge, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
var fileName string
|
var fileName string
|
||||||
@@ -75,23 +73,29 @@ func handleParserInstallCmd(ctx *ext.Context, u *ext.Update) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if fileName == "" {
|
if fileName == "" {
|
||||||
ctx.Reply(u, ext.ReplyTextString("无法获取文件名"), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserErrorGetFilenameFailed, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(fileName, ".js") {
|
if !strings.HasSuffix(fileName, ".js") {
|
||||||
ctx.Reply(u, ext.ReplyTextString("仅支持 .js 文件作为解析器"), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserErrorOnlyJsSupported, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
data := bytes.NewBuffer(nil)
|
data := bytes.NewBuffer(nil)
|
||||||
_, err := ctx.DownloadMedia(media, ext.DownloadOutputStream{Writer: data}, nil)
|
_, err := ctx.DownloadMedia(media, ext.DownloadOutputStream{Writer: data}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(u, ext.ReplyTextString("文件下载失败: "+err.Error()), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserErrorDownloadFileFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if err := parsers.AddPlugin(ctx, data.String(), fileName); err != nil {
|
if err := parsers.AddPlugin(ctx, data.String(), fileName); err != nil {
|
||||||
ctx.Reply(u, ext.ReplyTextString("插件安装失败: "+err.Error()), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserErrorInstallPluginFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(u, ext.ReplyTextString("插件安装成功: "+fileName), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgParserInfoInstallPluginSuccess, map[string]any{
|
||||||
|
"Name": fileName,
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,34 +10,35 @@ import (
|
|||||||
sabotfilters "github.com/krau/SaveAny-Bot/client/bot/handlers/utils/filters"
|
sabotfilters "github.com/krau/SaveAny-Bot/client/bot/handlers/utils/filters"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/re"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/re"
|
||||||
userclient "github.com/krau/SaveAny-Bot/client/user"
|
userclient "github.com/krau/SaveAny-Bot/client/user"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DescCommandHandler struct {
|
type DescCommandHandler struct {
|
||||||
Cmd string
|
Cmd string
|
||||||
Desc string
|
Desc i18nk.Key
|
||||||
handler func(ctx *ext.Context, u *ext.Update) error
|
handler func(ctx *ext.Context, u *ext.Update) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var CommandHandlers = []DescCommandHandler{
|
var CommandHandlers = []DescCommandHandler{
|
||||||
{"start", "开始使用", handleHelpCmd},
|
{"start", i18nk.BotMsgCmdStart, handleHelpCmd},
|
||||||
{"silent", "切换静默模式", handleSilentCmd},
|
{"silent", i18nk.BotMsgCmdSilent, handleSilentCmd},
|
||||||
{"storage", "设置默认存储端", handleStorageCmd},
|
{"storage", i18nk.BotMsgCmdStorage, handleStorageCmd},
|
||||||
{"dir", "管理存储文件夹", handleDirCmd},
|
{"dir", i18nk.BotMsgCmdDir, handleDirCmd},
|
||||||
{"rule", "管理自动存储规则", handleRuleCmd},
|
{"rule", i18nk.BotMsgCmdRule, handleRuleCmd},
|
||||||
{"save", "保存文件", handleSilentMode(handleSaveCmd, handleSilentSaveReplied)},
|
{"save", i18nk.BotMsgCmdSave, handleSilentMode(handleSaveCmd, handleSilentSaveReplied)},
|
||||||
{"dl", "下载给定链接的文件", handleDlCmd},
|
{"dl", i18nk.BotMsgCmdDl, handleDlCmd},
|
||||||
{"task", "管理任务队列", handleTaskCmd},
|
{"task", i18nk.BotMsgCmdTask, handleTaskCmd},
|
||||||
{"cancel", "取消任务", handleCancelCmd},
|
{"cancel", i18nk.BotMsgCmdCancel, handleCancelCmd},
|
||||||
{"watch", "监听聊天(UserBot)", handleWatchCmd},
|
{"watch", i18nk.BotMsgCmdWatch, handleWatchCmd},
|
||||||
{"unwatch", "取消监听聊天(UserBot)", handleUnwatchCmd},
|
{"unwatch", i18nk.BotMsgCmdUnwatch, handleUnwatchCmd},
|
||||||
{"lswatch", "列出监听的聊天(UserBot)", handleLswatchCmd},
|
{"lswatch", i18nk.BotMsgCmdLswatch, handleLswatchCmd},
|
||||||
{"config", "修改配置", handleConfigCmd},
|
{"config", i18nk.BotMsgCmdConfig, handleConfigCmd},
|
||||||
{"fnametmpl", "设置文件命名模板", handleConfigFnameTmpl},
|
{"fnametmpl", i18nk.BotMsgCmdFnametmpl, handleConfigFnameTmpl},
|
||||||
{"help", "显示帮助", handleHelpCmd},
|
{"help", i18nk.BotMsgCmdHelp, handleHelpCmd},
|
||||||
{"parser", "管理解析器", handleParserCmd},
|
{"parser", i18nk.BotMsgCmdParser, handleParserCmd},
|
||||||
{"update", "检查更新", handleUpdateCmd},
|
{"update", i18nk.BotMsgCmdUpdate, handleUpdateCmd},
|
||||||
}
|
}
|
||||||
|
|
||||||
func Register(disp dispatcher.Dispatcher) {
|
func Register(disp dispatcher.Dispatcher) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/strutil"
|
"github.com/krau/SaveAny-Bot/common/utils/strutil"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/rule"
|
"github.com/krau/SaveAny-Bot/pkg/rule"
|
||||||
@@ -21,8 +23,8 @@ func handleRuleCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
userChatID := update.GetUserChat().GetID()
|
userChatID := update.GetUserChat().GetID()
|
||||||
user, err := database.GetUserByChatID(ctx, userChatID)
|
user, err := database.GetUserByChatID(ctx, userChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("获取用户规则失败: %s", err)
|
logger.Errorf("Failed to get user rules: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取用户规则失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRuleErrorGetUserRulesFailed, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
@@ -34,11 +36,14 @@ func handleRuleCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
// /rule switch
|
// /rule switch
|
||||||
applyRule := !user.ApplyRule
|
applyRule := !user.ApplyRule
|
||||||
if err := database.UpdateUserApplyRule(ctx, user.ChatID, applyRule); err != nil {
|
if err := database.UpdateUserApplyRule(ctx, user.ChatID, applyRule); err != nil {
|
||||||
logger.Errorf("更新用户失败: %s", err)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRuleErrorUpdateUserFailed, nil)), nil)
|
||||||
ctx.Reply(update, ext.ReplyTextString("更新用户失败"), nil)
|
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("已%s规则模式", map[bool]string{true: "启用", false: "禁用"}[applyRule])), nil)
|
if applyRule {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRuleInfoRuleModeEnabled, nil)), nil)
|
||||||
|
} else {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRuleInfoRuleModeDisabled, nil)), nil)
|
||||||
|
}
|
||||||
case "add":
|
case "add":
|
||||||
// /rule add <type> <data> <storage> <dirpath>
|
// /rule add <type> <data> <storage> <dirpath>
|
||||||
if len(args) < 6 {
|
if len(args) < 6 {
|
||||||
@@ -52,10 +57,13 @@ func handleRuleCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rule.RuleType(""), fmt.Errorf("无效的规则类型: %s\n可用: %v", ruleTypeArg, slice.Join(rule.Values(), ", "))
|
return rule.RuleType(""), fmt.Errorf("invalid rule type: %s\navailable: %v", ruleTypeArg, slice.Join(rule.Values(), ", "))
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString(err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRuleErrorInvalidRuleType, map[string]any{
|
||||||
|
"Type": ruleTypeArg,
|
||||||
|
"Available": slice.Join(rule.Values(), ", "),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,29 +79,29 @@ func handleRuleCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
}
|
}
|
||||||
if err := database.CreateRule(ctx, rd); err != nil {
|
if err := database.CreateRule(ctx, rd); err != nil {
|
||||||
logger.Errorf("创建规则失败: %s", err)
|
logger.Errorf("failed to create rule: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("创建规则失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRuleErrorCreateRuleFailed, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString("创建规则成功"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRuleInfoCreateRuleSuccess, nil)), nil)
|
||||||
case "del":
|
case "del":
|
||||||
// /rule del <id>
|
// /rule del <id>
|
||||||
if len(args) < 3 {
|
if len(args) < 3 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("请提供规则ID"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRulePromptProvideRuleId, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ruleID := args[2]
|
ruleID := args[2]
|
||||||
id, err := strconv.Atoi(ruleID)
|
id, err := strconv.Atoi(ruleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("无效的规则ID"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRuleErrorInvalidRuleId, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if err := database.DeleteRule(ctx, uint(id)); err != nil {
|
if err := database.DeleteRule(ctx, uint(id)); err != nil {
|
||||||
logger.Errorf("删除规则失败: %s", err)
|
logger.Errorf("failed to delete rule %d: %s", id, err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("删除规则失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRuleErrorDeleteRuleFailed, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString("删除规则成功"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgRuleInfoDeleteRuleSuccess, nil)), nil)
|
||||||
default:
|
default:
|
||||||
ctx.Reply(update, ext.ReplyTextStyledTextArray(msgelem.BuildRuleHelpStyling(user.ApplyRule, user.Rules)), nil)
|
ctx.Reply(update, ext.ReplyTextStyledTextArray(msgelem.BuildRuleHelpStyling(user.ApplyRule, user.Rules)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -52,8 +51,8 @@ func handleSaveCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
stors := storage.GetUserStorages(ctx, userId)
|
stors := storage.GetUserStorages(ctx, userId)
|
||||||
req, err := msgelem.BuildAddOneSelectStorageMessage(ctx, stors, file, msg.ID)
|
req, err := msgelem.BuildAddOneSelectStorageMessage(ctx, stors, file, msg.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("构建存储选择消息失败: %s", err)
|
logger.Errorf("Failed to build storage selection message: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("构建存储选择消息失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorBuildStorageSelectMessageFailed, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), req)
|
ctx.EditMessage(update.EffectiveChat().GetID(), req)
|
||||||
@@ -97,35 +96,35 @@ func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error
|
|||||||
var err error
|
var err error
|
||||||
filter, err = regexp.Compile(filterStr)
|
filter, err = regexp.Compile(filterStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("无效的正则表达式: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorInvalidRegex, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startID, endID, err := strutil.ParseIntStrRange(msgIdRangeArg, "-")
|
startID, endID, err := strutil.ParseIntStrRange(msgIdRangeArg, "-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("无效的消息ID范围: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorInvalidMsgIdRange, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
chatID, err := tgutil.ParseChatID(ctx, chatArg)
|
chatID, err := tgutil.ParseChatID(ctx, chatArg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("无效的ID或用户名: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorInvalidIdOrUsername, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取消息..."), nil)
|
replied, err := ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonInfoFetchingMessages)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("回复失败: %s", err)
|
log.FromContext(ctx).Errorf("Failed to reply: %s", err)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
// [TODO]: generator istead of get all messages
|
// [TODO]: generator istead of get all messages
|
||||||
msgs, err := tgutil.GetMessagesRange(ctx, chatID, int(startID), int(endID))
|
msgs, err := tgutil.GetMessagesRange(ctx, chatID, int(startID), int(endID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取消息失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetMessagesFailed, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if len(msgs) == 0 {
|
if len(msgs) == 0 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("没有找到指定范围内的消息"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorNoMessagesInRange)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
files := make([]tfile.TGFileMessage, 0, len(msgs))
|
files := make([]tfile.TGFileMessage, 0, len(msgs))
|
||||||
@@ -144,7 +143,7 @@ func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error
|
|||||||
}
|
}
|
||||||
file, err := tfile.FromMediaMessage(media, ctx.Raw, msg, tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*msg)))
|
file, err := tfile.FromMediaMessage(media, ctx.Raw, msg, tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*msg)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("获取文件失败: %s", err)
|
log.FromContext(ctx).Errorf("Failed to get file from message: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if filter != nil {
|
if filter != nil {
|
||||||
@@ -160,7 +159,7 @@ func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error
|
|||||||
files = append(files, file)
|
files = append(files, file)
|
||||||
}
|
}
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("没有找到指定范围内的可保存消息"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorNoSavableMessagesInRange)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
stor := storage.FromContext(ctx)
|
stor := storage.FromContext(ctx)
|
||||||
@@ -171,16 +170,16 @@ func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error
|
|||||||
Files: files,
|
Files: files,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("构建存储选择键盘失败: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build storage selection keyboard: %s", err)
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
ID: replied.ID,
|
ID: replied.ID,
|
||||||
Message: "构建存储选择键盘失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorBuildStorageSelectKeyboardFailed, map[string]any{"Error": err.Error()}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
ID: replied.ID,
|
ID: replied.ID,
|
||||||
Message: fmt.Sprintf("找到 %d 个文件, 请选择存储位置", len(files)),
|
Message: i18n.T(i18nk.BotMsgCommonInfoFoundFilesSelectStorage, map[string]any{"Count": len(files)}),
|
||||||
ReplyMarkup: markup,
|
ReplyMarkup: markup,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
@@ -9,6 +8,8 @@ import (
|
|||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/common/cache"
|
"github.com/krau/SaveAny-Bot/common/cache"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
@@ -17,20 +18,27 @@ import (
|
|||||||
func handleSilentCmd(ctx *ext.Context, update *ext.Update) error {
|
func handleSilentCmd(ctx *ext.Context, update *ext.Update) error {
|
||||||
user, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
user, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取用户信息失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetUserInfoFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !user.Silent && user.DefaultStorage == "" {
|
if !user.Silent && user.DefaultStorage == "" {
|
||||||
ctx.Reply(update, ext.ReplyTextString("请先使用 /storage 设置默认存储位置"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorDefaultStorageNotSet, nil)), nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
user.Silent = !user.Silent
|
user.Silent = !user.Silent
|
||||||
if err := database.UpdateUser(ctx, user); err != nil {
|
if err := database.UpdateUser(ctx, user); err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("更新用户信息失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorUpdateUserInfoFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
responseText := "已" + map[bool]string{true: "开启", false: "关闭"}[user.Silent] + "静默模式"
|
if user.Silent {
|
||||||
ctx.Reply(update, ext.ReplyTextString(responseText), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonInfoSilentModeOn, nil)), nil)
|
||||||
|
} else {
|
||||||
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonInfoSilentModeOff, nil)), nil)
|
||||||
|
}
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,18 +57,22 @@ func handleSetDefaultCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return failedAnswer("数据已过期")
|
return failedAnswer(i18n.T(i18nk.BotMsgCommonErrorDataExpired, nil))
|
||||||
}
|
}
|
||||||
userID := update.CallbackQuery.GetUserID()
|
userID := update.CallbackQuery.GetUserID()
|
||||||
|
|
||||||
storageName := data.StorageName
|
storageName := data.StorageName
|
||||||
selectedStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, storageName)
|
selectedStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, storageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failedAnswer("存储获取失败: " + err.Error())
|
return failedAnswer(i18n.T(i18nk.BotMsgCommonErrorGetStorageFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
user, err := database.GetUserByChatID(ctx, userID)
|
user, err := database.GetUserByChatID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failedAnswer("获取用户信息失败: " + err.Error())
|
return failedAnswer(i18n.T(i18nk.BotMsgCommonErrorGetUserInfoFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
var dir *database.Dir
|
var dir *database.Dir
|
||||||
if data.DirID != 0 {
|
if data.DirID != 0 {
|
||||||
@@ -68,24 +80,28 @@ func handleSetDefaultCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
var err error
|
var err error
|
||||||
dir, err = database.GetDirByID(ctx, data.DirID)
|
dir, err = database.GetDirByID(ctx, data.DirID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failedAnswer("获取文件夹信息失败: " + err.Error())
|
return failedAnswer(i18n.T(i18nk.BotMsgDirErrorGetUserDirsFailed, nil))
|
||||||
}
|
}
|
||||||
user.DefaultDir = dir.ID
|
user.DefaultDir = dir.ID
|
||||||
} else {
|
} else {
|
||||||
// 检查是否有可用的文件夹
|
// 检查是否有可用的文件夹
|
||||||
dirs, err := database.GetDirsByUserIDAndStorageName(ctx, user.ID, storageName)
|
dirs, err := database.GetDirsByUserIDAndStorageName(ctx, user.ID, storageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failedAnswer("获取目录失败: " + err.Error())
|
return failedAnswer(i18n.T(i18nk.BotMsgCommonErrorGetDirFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
if len(dirs) > 0 {
|
if len(dirs) > 0 {
|
||||||
// 要求选择文件夹
|
// 要求选择文件夹
|
||||||
markup, err := msgelem.BuildSetDefaultDirMarkup(ctx, storageName, dirs)
|
markup, err := msgelem.BuildSetDefaultDirMarkup(ctx, storageName, dirs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failedAnswer("构建目录选择失败: " + err.Error())
|
return failedAnswer(i18n.T(i18nk.BotMsgCommonErrorBuildDirSelectKeyboardFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: update.CallbackQuery.GetMsgID(),
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
Message: "请选择要保存到的默认文件夹",
|
Message: i18n.T(i18nk.BotMsgCommonPromptSelectDefaultDir, nil),
|
||||||
ReplyMarkup: markup,
|
ReplyMarkup: markup,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
@@ -93,11 +109,18 @@ func handleSetDefaultCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
}
|
}
|
||||||
user.DefaultStorage = selectedStorage.Name()
|
user.DefaultStorage = selectedStorage.Name()
|
||||||
if err := database.UpdateUser(ctx, user); err != nil {
|
if err := database.UpdateUser(ctx, user); err != nil {
|
||||||
return failedAnswer("更新用户信息失败: " + err.Error())
|
return failedAnswer(i18n.T(i18nk.BotMsgCommonErrorUpdateUserInfoFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
msg := fmt.Sprintf("已将默认存储位置设为: %s", selectedStorage.Name())
|
msg := i18n.T(i18nk.BotMsgCommonInfoDefaultStorageSet, map[string]any{
|
||||||
|
"Name": selectedStorage.Name(),
|
||||||
|
})
|
||||||
if dir != nil {
|
if dir != nil {
|
||||||
msg += fmt.Sprintf(":/%s", strings.TrimPrefix(dir.Path, "/"))
|
msg = i18n.T(i18nk.BotMsgCommonInfoDefaultStorageWithDirSet, map[string]any{
|
||||||
|
"Name": selectedStorage.Name(),
|
||||||
|
"Dir": strings.TrimPrefix(dir.Path, "/"),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: update.CallbackQuery.GetMsgID(),
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
@@ -110,15 +133,17 @@ func handleStorageCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
userID := update.GetUserChat().GetID()
|
userID := update.GetUserChat().GetID()
|
||||||
storages := storage.GetUserStorages(ctx, userID)
|
storages := storage.GetUserStorages(ctx, userID)
|
||||||
if len(storages) == 0 {
|
if len(storages) == 0 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorNoAvailableStorage, nil)), nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
markup, err := msgelem.BuildSetDefaultStorageMarkup(ctx, storages)
|
markup, err := msgelem.BuildSetDefaultStorageMarkup(ctx, storages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取存储失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetStorageFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString("请选择要设为默认的存储位置"), &ext.ReplyOpts{
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonPromptSelectDefaultStorage, nil)), &ext.ReplyOpts{
|
||||||
Markup: markup,
|
Markup: markup,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -9,6 +8,8 @@ import (
|
|||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/core"
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,21 +28,21 @@ func handleTaskCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
showQueuedTasks(ctx, update)
|
showQueuedTasks(ctx, update)
|
||||||
case "cancel", "c":
|
case "cancel", "c":
|
||||||
if len(args) < 3 {
|
if len(args) < 3 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("用法: /tasks cancel <task_id>"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTasksUsageCancel)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
taskID := args[2]
|
taskID := args[2]
|
||||||
if err := core.CancelTask(ctx, taskID); err != nil {
|
if err := core.CancelTask(ctx, taskID); err != nil {
|
||||||
logger.Errorf("取消任务 %s 失败: %v", taskID, err)
|
logger.Errorf("Failed to cancel task %s: %v", taskID, err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("取消任务失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTasksCancelFailed, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextStyledTextArray([]styling.StyledTextOption{
|
ctx.Reply(update, ext.ReplyTextStyledTextArray([]styling.StyledTextOption{
|
||||||
styling.Plain("已请求取消任务: "),
|
styling.Plain(i18n.T(i18nk.BotMsgTasksCancelRequestedPrefix)),
|
||||||
styling.Code(taskID),
|
styling.Code(taskID),
|
||||||
}), nil)
|
}), nil)
|
||||||
default:
|
default:
|
||||||
ctx.Reply(update, ext.ReplyTextString("用法: /tasks [running|queued|cancel <task_id>]"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTasksUsage)), nil)
|
||||||
}
|
}
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -49,28 +50,28 @@ func handleTaskCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
func showRunningTasks(ctx *ext.Context, update *ext.Update) {
|
func showRunningTasks(ctx *ext.Context, update *ext.Update) {
|
||||||
tasks := core.GetRunningTasks(ctx)
|
tasks := core.GetRunningTasks(ctx)
|
||||||
if len(tasks) == 0 {
|
if len(tasks) == 0 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("当前没有正在运行的任务"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTasksRunningEmpty)), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
opts := make([]styling.StyledTextOption, 0, 2+len(tasks)*4)
|
opts := make([]styling.StyledTextOption, 0, 2+len(tasks)*4)
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
styling.Bold("当前正在运行的任务:"),
|
styling.Bold(i18n.T(i18nk.BotMsgTasksRunningTitle)),
|
||||||
styling.Plain(fmt.Sprintf("\n总数: %d\n", len(tasks))),
|
styling.Plain(i18n.T(i18nk.BotMsgTasksTotalPrefix, map[string]any{"Count": len(tasks)})),
|
||||||
)
|
)
|
||||||
for _, t := range tasks {
|
for _, t := range tasks {
|
||||||
created := t.Created.In(time.Local).Format("2006-01-02 15:04:05")
|
created := t.Created.In(time.Local).Format("2006-01-02 15:04:05")
|
||||||
status := "运行中"
|
status := i18n.T(i18nk.BotMsgTasksStatusRunning)
|
||||||
if t.Cancelled {
|
if t.Cancelled {
|
||||||
status = "已请求取消"
|
status = i18n.T(i18nk.BotMsgTasksStatusCancelRequested)
|
||||||
}
|
}
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
styling.Plain("\nID: "),
|
styling.Plain("\n"+i18n.T(i18nk.BotMsgTasksFieldId)),
|
||||||
styling.Code(t.ID),
|
styling.Code(t.ID),
|
||||||
styling.Plain("\n名称: "),
|
styling.Plain("\n"+i18n.T(i18nk.BotMsgTasksFieldTitle)),
|
||||||
styling.Code(t.Title),
|
styling.Code(t.Title),
|
||||||
styling.Plain("\n创建时间: "),
|
styling.Plain("\n"+i18n.T(i18nk.BotMsgTasksFieldCreated)),
|
||||||
styling.Code(created),
|
styling.Code(created),
|
||||||
styling.Plain("\n状态: "),
|
styling.Plain("\n"+i18n.T(i18nk.BotMsgTasksFieldStatus)),
|
||||||
styling.Code(status),
|
styling.Code(status),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -80,32 +81,32 @@ func showRunningTasks(ctx *ext.Context, update *ext.Update) {
|
|||||||
func showQueuedTasks(ctx *ext.Context, update *ext.Update) {
|
func showQueuedTasks(ctx *ext.Context, update *ext.Update) {
|
||||||
tasks := core.GetQueuedTasks(ctx)
|
tasks := core.GetQueuedTasks(ctx)
|
||||||
if len(tasks) == 0 {
|
if len(tasks) == 0 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("当前没有排队中的任务"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTasksQueuedEmpty)), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
opts := make([]styling.StyledTextOption, 0, 2+len(tasks)*3)
|
opts := make([]styling.StyledTextOption, 0, 2+len(tasks)*3)
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
styling.Bold("当前排队中的任务:"),
|
styling.Bold(i18n.T(i18nk.BotMsgTasksQueuedTitle)),
|
||||||
styling.Plain(fmt.Sprintf("\n总数: %d\n", len(tasks))),
|
styling.Plain(i18n.T(i18nk.BotMsgTasksTotalPrefix, map[string]any{"Count": len(tasks)})),
|
||||||
)
|
)
|
||||||
for _, t := range tasks {
|
for _, t := range tasks {
|
||||||
created := t.Created.In(time.Local).Format("2006-01-02 15:04:05")
|
created := t.Created.In(time.Local).Format("2006-01-02 15:04:05")
|
||||||
status := "排队中"
|
status := i18n.T(i18nk.BotMsgTasksStatusQueued)
|
||||||
if t.Cancelled {
|
if t.Cancelled {
|
||||||
status = "已请求取消"
|
status = i18n.T(i18nk.BotMsgTasksStatusCancelRequested)
|
||||||
}
|
}
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
styling.Plain("\nID: "),
|
styling.Plain("\n"+i18n.T(i18nk.BotMsgTasksFieldId)),
|
||||||
styling.Code(t.ID),
|
styling.Code(t.ID),
|
||||||
styling.Plain("\n名称: "),
|
styling.Plain("\n"+i18n.T(i18nk.BotMsgTasksFieldTitle)),
|
||||||
styling.Code(t.Title),
|
styling.Code(t.Title),
|
||||||
styling.Plain("\n创建时间: "),
|
styling.Plain("\n"+i18n.T(i18nk.BotMsgTasksFieldCreated)),
|
||||||
styling.Code(created),
|
styling.Code(created),
|
||||||
styling.Plain("\n状态: "),
|
styling.Plain("\n"+i18n.T(i18nk.BotMsgTasksFieldStatus)),
|
||||||
styling.Code(status),
|
styling.Code(status),
|
||||||
)
|
)
|
||||||
if len(tasks) > 10 {
|
if len(tasks) > 10 {
|
||||||
opts = append(opts, styling.Plain("\n...\n只显示前 10 个任务, 共 "+fmt.Sprintf("%d", len(tasks))+" 个任务"))
|
opts = append(opts, styling.Plain("\n"+i18n.T(i18nk.BotMsgTasksTruncatedNote, map[string]any{"Count": len(tasks)})))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
@@ -34,18 +36,20 @@ func handleTelegraphUrlMessage(ctx *ext.Context, update *ext.Update) error {
|
|||||||
TphPics: result.Pics,
|
TphPics: result.Pics,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("构建存储选择键盘失败: %s", err)
|
logger.Errorf("Failed to build storage selection keyboard: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("构建存储选择键盘失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTelegraphErrorBuildStorageSelectKeyboardFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
eb := entity.Builder{}
|
eb := entity.Builder{}
|
||||||
if err := styling.Perform(&eb,
|
if err := styling.Perform(&eb,
|
||||||
styling.Plain("标题: "),
|
styling.Plain(i18n.T(i18nk.BotMsgTelegraphInfoTitlePrefix, nil)),
|
||||||
styling.Code(result.Page.Title),
|
styling.Code(result.Page.Title),
|
||||||
styling.Plain("\n图片数量: "),
|
styling.Plain(i18n.T(i18nk.BotMsgTelegraphInfoPicCountPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%d", len(result.Pics))),
|
styling.Code(fmt.Sprintf("%d", len(result.Pics))),
|
||||||
styling.Plain("\n请选择存储位置"),
|
styling.Plain(i18n.T(i18nk.BotMsgTelegraphInfoPromptSelectStorage, nil)),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entity: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entity: %s", err)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -11,6 +10,8 @@ import (
|
|||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/gotd/td/telegram/message/html"
|
"github.com/gotd/td/telegram/message/html"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/unvgo/ghselfupdate"
|
"github.com/unvgo/ghselfupdate"
|
||||||
)
|
)
|
||||||
@@ -18,24 +19,33 @@ import (
|
|||||||
func handleUpdateCmd(ctx *ext.Context, u *ext.Update) error {
|
func handleUpdateCmd(ctx *ext.Context, u *ext.Update) error {
|
||||||
currentV, err := semver.Parse(config.Version)
|
currentV, err := semver.Parse(config.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("You are in dev or the version var failed to inject: %v", err)), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgUpdateErrorVersionVarInvalid, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
latest, ok, err := ghselfupdate.DetectLatest(config.GitRepo)
|
latest, ok, err := ghselfupdate.DetectLatest(config.GitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("检测最新版本失败: %v", err)), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgUpdateErrorCheckLatestFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
ctx.Reply(u, ext.ReplyTextString("没有找到版本信息"), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgUpdateErrorNoReleaseFound, nil)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if latest.Version.Major != currentV.Major {
|
if latest.Version.Major != currentV.Major {
|
||||||
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("检测到大版本更新: %s -> %s , 请前往 GitHub 手动下载最新版本并查看迁移指南", currentV, latest.Version)), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgUpdateInfoMajorUpgradeRequired, map[string]any{
|
||||||
|
"Current": currentV.String(),
|
||||||
|
"Latest": latest.Version.String(),
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if latest.Version.LT(currentV) || latest.Version.Equals(currentV) {
|
if latest.Version.LT(currentV) || latest.Version.Equals(currentV) {
|
||||||
ctx.Reply(u, ext.ReplyTextString(fmt.Sprintf("当前已经是最新版本: %s", config.Version)), nil)
|
ctx.Reply(u, ext.ReplyTextString(i18n.T(i18nk.BotMsgUpdateInfoAlreadyLatest, map[string]any{
|
||||||
|
"Version": config.Version,
|
||||||
|
})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
indocker := config.Docker == "true"
|
indocker := config.Docker == "true"
|
||||||
@@ -55,32 +65,28 @@ func handleUpdateCmd(ctx *ext.Context, u *ext.Update) error {
|
|||||||
return `<blockquote expandable>` + md + `</blockquote>`
|
return `<blockquote expandable>` + md + `</blockquote>`
|
||||||
}()))
|
}()))
|
||||||
if indocker {
|
if indocker {
|
||||||
text := fmt.Sprintf("发现新版本: %s\n当前版本: %s\n发布时间: %s\n由于您正在使用 Docker 部署, 请自行在部署平台上执行更新命令",
|
text := i18n.T(i18nk.BotMsgUpdateInfoNewVersionInDocker, map[string]any{
|
||||||
latest.Version,
|
"Latest": latest.Version.String(),
|
||||||
config.Version,
|
"Current": config.Version,
|
||||||
latest.PublishedAt.Format("2006-01-02 15:04:05"),
|
"PublishedAt": latest.PublishedAt.Format("2006-01-02 15:04:05"),
|
||||||
)
|
})
|
||||||
ctx.Reply(u, ext.ReplyTextString(text), nil)
|
ctx.Reply(u, ext.ReplyTextString(text), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
text := fmt.Sprintf(`发现新版本: %s
|
text := i18n.T(i18nk.BotMsgUpdateInfoNewVersionPromptUpgrade, map[string]any{
|
||||||
当前版本: %s
|
"Latest": latest.Version.String(),
|
||||||
|
"Current": config.Version,
|
||||||
文件大小: %.2f MB
|
"SizeMB": float64(latest.AssetByteSize) / (1024 * 1024),
|
||||||
下载链接: %s
|
"URL": latest.AssetURL,
|
||||||
发布时间: %s
|
"PublishedAt": latest.PublishedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
升级将重启 Bot , 是否升级?`, latest.Version, config.Version,
|
|
||||||
float64(latest.AssetByteSize)/(1024*1024), latest.AssetURL,
|
|
||||||
latest.PublishedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
)
|
|
||||||
ctx.Reply(u, ext.ReplyTextString(text), &ext.ReplyOpts{
|
ctx.Reply(u, ext.ReplyTextString(text), &ext.ReplyOpts{
|
||||||
Markup: &tg.ReplyInlineMarkup{
|
Markup: &tg.ReplyInlineMarkup{
|
||||||
Rows: []tg.KeyboardButtonRow{
|
Rows: []tg.KeyboardButtonRow{
|
||||||
{
|
{
|
||||||
Buttons: []tg.KeyboardButtonClass{
|
Buttons: []tg.KeyboardButtonClass{
|
||||||
&tg.KeyboardButtonCallback{
|
&tg.KeyboardButtonCallback{
|
||||||
Text: "升级",
|
Text: i18n.T(i18nk.BotMsgUpdateButtonUpgrade, nil),
|
||||||
Data: []byte("update"),
|
Data: []byte("update"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -98,19 +104,25 @@ func handleUpdateCallback(ctx *ext.Context, u *ext.Update) error {
|
|||||||
}
|
}
|
||||||
ctx.EditMessage(u.GetUserChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(u.GetUserChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
ID: u.CallbackQuery.GetMsgID(),
|
ID: u.CallbackQuery.GetMsgID(),
|
||||||
Message: fmt.Sprintf("正在升级中, 当前版本: %s", config.Version),
|
Message: i18n.T(i18nk.BotMsgUpdateInfoUpgradingWithVersion, map[string]any{
|
||||||
|
"Current": config.Version,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
latest, err := ghselfupdate.UpdateSelf(currentV, config.GitRepo)
|
latest, err := ghselfupdate.UpdateSelf(currentV, config.GitRepo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.EditMessage(u.GetUserChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(u.GetUserChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
ID: u.CallbackQuery.GetMsgID(),
|
ID: u.CallbackQuery.GetMsgID(),
|
||||||
Message: fmt.Sprintf("升级失败: %v", err),
|
Message: i18n.T(i18nk.BotMsgUpdateErrorUpgradeFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.EditMessage(u.GetUserChat().GetID(), &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(u.GetUserChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||||
ID: u.CallbackQuery.GetMsgID(),
|
ID: u.CallbackQuery.GetMsgID(),
|
||||||
Message: fmt.Sprintf("已升级至版本 %s\n若 Bot 未自动重启请手动启动", latest.Version),
|
Message: i18n.T(i18nk.BotMsgUpdateInfoUpgradeSuccess, map[string]any{
|
||||||
|
"Version": latest.Version.String(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return errors.New("SAVEANTBOT-RESTART")
|
return errors.New("SAVEANTBOT-RESTART")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,22 +5,24 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BuildDirHelpStyling(dirs []database.Dir) []styling.StyledTextOption {
|
func BuildDirHelpStyling(dirs []database.Dir) []styling.StyledTextOption {
|
||||||
return []styling.StyledTextOption{
|
return []styling.StyledTextOption{
|
||||||
styling.Bold("使用方法: /dir <操作> <参数...>"),
|
styling.Bold(i18n.T(i18nk.BotMsgDirHelpUsage, nil)),
|
||||||
styling.Plain("\n\n可用操作:\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgDirHelpAvailableOps, nil)),
|
||||||
styling.Code("add"),
|
styling.Code("add"),
|
||||||
styling.Plain(" <存储名> <路径> - 添加路径\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgDirHelpAddSuffix, nil)),
|
||||||
styling.Code("del"),
|
styling.Code("del"),
|
||||||
styling.Plain(" <路径ID> - 删除路径\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgDirHelpDelSuffix, nil)),
|
||||||
styling.Plain("\n添加路径示例:\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgDirHelpAddExamplePrefix, nil)),
|
||||||
styling.Code("/dir add local1 path/to/dir"),
|
styling.Code(i18n.T(i18nk.BotMsgDirHelpAddExampleCmd, nil)),
|
||||||
styling.Plain("\n\n删除路径示例:\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgDirHelpDelExamplePrefix, nil)),
|
||||||
styling.Code("/dir del 3"),
|
styling.Code(i18n.T(i18nk.BotMsgDirHelpDelExampleCmd, nil)),
|
||||||
styling.Plain("\n\n当前已添加的路径:\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgDirHelpExistingDirsPrefix, nil)),
|
||||||
styling.Blockquote(func() string {
|
styling.Blockquote(func() string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, dir := range dirs {
|
for _, dir := range dirs {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/parser"
|
"github.com/krau/SaveAny-Bot/pkg/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,15 +16,15 @@ func BuildParsedTextEntity(item parser.Item) (string, []tg.MessageEntityClass, e
|
|||||||
eb := entity.Builder{}
|
eb := entity.Builder{}
|
||||||
if err := styling.Perform(&eb,
|
if err := styling.Perform(&eb,
|
||||||
styling.Bold(fmt.Sprintf("[%s]%s", item.Site, item.Title)),
|
styling.Bold(fmt.Sprintf("[%s]%s", item.Site, item.Title)),
|
||||||
styling.Plain("\n链接: "),
|
styling.Plain(i18n.T(i18nk.BotMsgParseInfoLinkPrefix, nil)),
|
||||||
styling.Code(item.URL),
|
styling.Code(item.URL),
|
||||||
styling.Plain("\n作者: "),
|
styling.Plain(i18n.T(i18nk.BotMsgParseInfoAuthorPrefix, nil)),
|
||||||
styling.Code(item.Author),
|
styling.Code(item.Author),
|
||||||
styling.Plain("\n描述: "),
|
styling.Plain(i18n.T(i18nk.BotMsgParseInfoDescriptionPrefix, nil)),
|
||||||
styling.Code(strutil.Ellipsis(item.Description, 233)),
|
styling.Code(strutil.Ellipsis(item.Description, 233)),
|
||||||
styling.Plain("\n文件数量: "),
|
styling.Plain(i18n.T(i18nk.BotMsgParseInfoFileCountPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%d", len(item.Resources))),
|
styling.Code(fmt.Sprintf("%d", len(item.Resources))),
|
||||||
styling.Plain("\n预计总大小: "),
|
styling.Plain(i18n.T(i18nk.BotMsgParseInfoTotalSizePrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%.2f MB", func() float64 {
|
styling.Code(fmt.Sprintf("%.2f MB", func() float64 {
|
||||||
var totalSize int64
|
var totalSize int64
|
||||||
for _, res := range item.Resources {
|
for _, res := range item.Resources {
|
||||||
@@ -30,9 +32,9 @@ func BuildParsedTextEntity(item parser.Item) (string, []tg.MessageEntityClass, e
|
|||||||
}
|
}
|
||||||
return float64(totalSize) / 1024 / 1024
|
return float64(totalSize) / 1024 / 1024
|
||||||
}())),
|
}())),
|
||||||
styling.Plain("\n请选择存储位置"),
|
styling.Plain(i18n.T(i18nk.BotMsgParseInfoPromptSelectStorage, nil)),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return "", nil, fmt.Errorf("构建消息失败: %w", err)
|
return "", nil, fmt.Errorf("failed to build parsed text entity: %w", err)
|
||||||
}
|
}
|
||||||
text, entities := eb.Complete()
|
text, entities := eb.Complete()
|
||||||
return text, entities, nil
|
return text, entities, nil
|
||||||
|
|||||||
@@ -5,21 +5,28 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BuildRuleHelpStyling(enabled bool, rules []database.Rule) []styling.StyledTextOption {
|
func BuildRuleHelpStyling(enabled bool, rules []database.Rule) []styling.StyledTextOption {
|
||||||
return []styling.StyledTextOption{
|
return []styling.StyledTextOption{
|
||||||
styling.Bold("使用方法: /rule <操作> <参数...>"),
|
styling.Bold(i18n.T(i18nk.BotMsgRuleHelpUsage, nil)),
|
||||||
styling.Bold(fmt.Sprintf("\n当前已%s规则模式", map[bool]string{true: "启用", false: "禁用"}[enabled])),
|
styling.Bold(func() string {
|
||||||
styling.Plain("\n\n可用操作:\n"),
|
if enabled {
|
||||||
|
return i18n.T(i18nk.BotMsgRuleHelpCurrentModeEnabled, nil)
|
||||||
|
}
|
||||||
|
return i18n.T(i18nk.BotMsgRuleHelpCurrentModeDisabled, nil)
|
||||||
|
}()),
|
||||||
|
styling.Plain(i18n.T(i18nk.BotMsgRuleHelpAvailableOps, nil)),
|
||||||
styling.Code("switch"),
|
styling.Code("switch"),
|
||||||
styling.Plain(" - 开关规则模式\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgRuleHelpSwitchSuffix, nil)),
|
||||||
styling.Code("add"),
|
styling.Code("add"),
|
||||||
styling.Plain(" <类型> <数据> <存储名> <路径> - 添加规则\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgRuleHelpAddSuffix, nil)),
|
||||||
styling.Code("del"),
|
styling.Code("del"),
|
||||||
styling.Plain(" <规则ID> - 删除规则\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgRuleHelpDelSuffix, nil)),
|
||||||
styling.Plain("\n当前已添加的规则:\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgRuleHelpExistingRulesPrefix, nil)),
|
||||||
styling.Blockquote(func() string {
|
styling.Blockquote(func() string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/common/cache"
|
"github.com/krau/SaveAny-Bot/common/cache"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
@@ -70,11 +72,14 @@ func BuildAddSelectStorageKeyboard(stors []storage.Storage, adddata tcbdata.Add)
|
|||||||
func BuildAddOneSelectStorageMessage(ctx context.Context, stors []storage.Storage, file tfile.TGFileMessage, msgId int) (*tg.MessagesEditMessageRequest, error) {
|
func BuildAddOneSelectStorageMessage(ctx context.Context, stors []storage.Storage, file tfile.TGFileMessage, msgId int) (*tg.MessagesEditMessageRequest, error) {
|
||||||
eb := entity.Builder{}
|
eb := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
text := fmt.Sprintf("文件名: %s\n请选择存储位置", file.Name())
|
text := i18n.T(i18nk.BotMsgTasksInfoAddedToQueueFull, map[string]any{
|
||||||
|
"Filename": file.Name(),
|
||||||
|
"QueueLength": 0,
|
||||||
|
})
|
||||||
if err := styling.Perform(&eb,
|
if err := styling.Perform(&eb,
|
||||||
styling.Plain("文件名: "),
|
styling.Plain(i18n.T(i18nk.BotMsgStorageInfoFilenamePrefix, nil)),
|
||||||
styling.Code(file.Name()),
|
styling.Code(file.Name()),
|
||||||
styling.Plain("\n请选择存储位置"),
|
styling.Plain(i18n.T(i18nk.BotMsgStorageInfoPromptSelectStorage, nil)),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entity: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entity: %s", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -185,7 +190,7 @@ func BuildSetDirMarkupForAdd(dirs []database.Dir, dataid string) (*tg.ReplyInlin
|
|||||||
return nil, fmt.Errorf("failed to set default directory data in cache: %w", err)
|
return nil, fmt.Errorf("failed to set default directory data in cache: %w", err)
|
||||||
}
|
}
|
||||||
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
||||||
Text: "默认",
|
Text: i18n.T(i18nk.BotMsgDirButtonDefault, nil),
|
||||||
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeAdd, dirDefaultDataId),
|
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeAdd, dirDefaultDataId),
|
||||||
})
|
})
|
||||||
markup := &tg.ReplyInlineMarkup{}
|
markup := &tg.ReplyInlineMarkup{}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ package msgelem
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BuildTaskAddedEntities(
|
func BuildTaskAddedEntities(
|
||||||
@@ -18,11 +19,15 @@ func BuildTaskAddedEntities(
|
|||||||
) (string, []tg.MessageEntityClass) {
|
) (string, []tg.MessageEntityClass) {
|
||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
text := fmt.Sprintf("已添加到任务队列\n文件名: %s\n当前排队任务数: %d", filename, queueLength)
|
text := i18n.T(i18nk.BotMsgTasksInfoAddedToQueueFull, map[string]any{
|
||||||
|
"Filename": filename,
|
||||||
|
"QueueLength": queueLength,
|
||||||
|
})
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("已添加到任务队列\n文件名: "),
|
styling.Plain(i18n.T(i18nk.BotMsgTasksInfoAddedToQueuePrefix, nil)),
|
||||||
|
styling.Plain(i18n.T(i18nk.BotMsgTasksInfoFilenamePrefix, nil)),
|
||||||
styling.Code(filename),
|
styling.Code(filename),
|
||||||
styling.Plain("\n当前排队任务数: "),
|
styling.Plain(i18n.T(i18nk.BotMsgTasksInfoQueueLengthPrefix, nil)),
|
||||||
styling.Bold(strconv.Itoa(queueLength)),
|
styling.Bold(strconv.Itoa(queueLength)),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entity: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entity: %s", err)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/core"
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
"github.com/krau/SaveAny-Bot/core/tasks/directlinks"
|
"github.com/krau/SaveAny-Bot/core/tasks/directlinks"
|
||||||
@@ -18,13 +20,15 @@ func CreateAndAddDirectTaskWithEdit(ctx *ext.Context, stor storage.Storage, dirP
|
|||||||
if err := core.AddTask(injectCtx, task); err != nil {
|
if err := core.AddTask(injectCtx, task); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to add task: %s", err)
|
log.FromContext(ctx).Errorf("Failed to add task: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: msgID,
|
ID: msgID,
|
||||||
Message: "任务添加失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorTaskAddFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
Message: "任务已添加",
|
Message: i18n.T(i18nk.BotMsgCommonInfoTaskAdded, nil),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/re"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/re"
|
||||||
uc "github.com/krau/SaveAny-Bot/client/user"
|
uc "github.com/krau/SaveAny-Bot/client/user"
|
||||||
"github.com/krau/SaveAny-Bot/common/cache"
|
"github.com/krau/SaveAny-Bot/common/cache"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tphutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tphutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
@@ -36,7 +38,7 @@ func GetFileFromMessageWithReply(ctx *ext.Context, update *ext.Update, message *
|
|||||||
return nil, nil, dispatcher.ContinueGroups
|
return nil, nil, dispatcher.ContinueGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
replied, err = ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
|
replied, err = ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonInfoFetchingFileInfo, nil)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to reply: %s", err)
|
logger.Errorf("Failed to reply: %s", err)
|
||||||
return nil, nil, dispatcher.EndGroups
|
return nil, nil, dispatcher.EndGroups
|
||||||
@@ -52,7 +54,9 @@ func GetFileFromMessageWithReply(ctx *ext.Context, update *ext.Update, message *
|
|||||||
file, err = tfile.FromMediaMessage(media, ctx.Raw, message, tfileopts...)
|
file, err = tfile.FromMediaMessage(media, ctx.Raw, message, tfileopts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to get file from media: %s", err)
|
logger.Errorf("Failed to get file from media: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取文件失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetFileFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return nil, nil, dispatcher.EndGroups
|
return nil, nil, dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
return replied, file, nil
|
return replied, file, nil
|
||||||
@@ -68,7 +72,7 @@ func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Up
|
|||||||
logger.Warn("no matched message links but called handleMessageLink")
|
logger.Warn("no matched message links but called handleMessageLink")
|
||||||
return nil, nil, nil, dispatcher.EndGroups
|
return nil, nil, nil, dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
replied, err = ctx.Reply(update, ext.ReplyTextString("正在获取消息..."), nil)
|
replied, err = ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonInfoFetchingMessages, nil)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("failed to reply: %s", err)
|
logger.Errorf("failed to reply: %s", err)
|
||||||
return nil, nil, nil, dispatcher.EndGroups
|
return nil, nil, nil, dispatcher.EndGroups
|
||||||
@@ -85,7 +89,9 @@ func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Up
|
|||||||
user, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
user, err := database.GetUserByChatID(ctx, update.GetUserChat().GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("failed to get user from db: %s", err)
|
logger.Errorf("failed to get user from db: %s", err)
|
||||||
editReplied("获取用户信息失败: "+err.Error(), nil)
|
editReplied(i18n.T(i18nk.BotMsgCommonErrorGetUserInfoFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}), nil)
|
||||||
return nil, nil, nil, dispatcher.EndGroups
|
return nil, nil, nil, dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
files = make([]tfile.TGFileMessage, 0, len(msgLinks))
|
files = make([]tfile.TGFileMessage, 0, len(msgLinks))
|
||||||
@@ -110,7 +116,9 @@ func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Up
|
|||||||
|
|
||||||
tctx := ctx
|
tctx := ctx
|
||||||
if config.C().Telegram.Userbot.Enable {
|
if config.C().Telegram.Userbot.Enable {
|
||||||
tctx = uc.GetCtx()
|
if uc.GetCtx() != nil {
|
||||||
|
tctx = uc.GetCtx()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, link := range msgLinks {
|
for _, link := range msgLinks {
|
||||||
@@ -144,7 +152,7 @@ func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Up
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
editReplied("没有找到可保存的文件", nil)
|
editReplied(i18n.T(i18nk.BotMsgCommonErrorNoSavableFilesFound, nil), nil)
|
||||||
return nil, nil, nil, dispatcher.EndGroups
|
return nil, nil, nil, dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
return replied, files, editReplied, nil
|
return replied, files, editReplied, nil
|
||||||
@@ -155,7 +163,7 @@ func GetCallbackDataWithAnswer[DataType any](ctx *ext.Context, update *ext.Updat
|
|||||||
if !ok {
|
if !ok {
|
||||||
log.FromContext(ctx).Warnf("Invalid data ID: %s", dataid)
|
log.FromContext(ctx).Warnf("Invalid data ID: %s", dataid)
|
||||||
queryID := update.CallbackQuery.GetQueryID()
|
queryID := update.CallbackQuery.GetQueryID()
|
||||||
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, "数据已过期或无效"))
|
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, i18n.T(i18nk.BotMsgCommonErrorDataExpired, nil)))
|
||||||
var zero DataType
|
var zero DataType
|
||||||
return zero, dispatcher.EndGroups
|
return zero, dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -180,11 +188,13 @@ func GetTphPicsFromMessageWithReply(ctx *ext.Context, update *ext.Update) (*type
|
|||||||
tphdir, err := url.PathUnescape(pagepath)
|
tphdir, err := url.PathUnescape(pagepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to unescape telegraph path: %s", err)
|
logger.Errorf("Failed to unescape telegraph path: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("解析 telegraph 路径失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorParseTelegraphPathFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return nil, nil, dispatcher.EndGroups
|
return nil, nil, dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
tphdir = strings.TrimSpace(tphdir)
|
tphdir = strings.TrimSpace(tphdir)
|
||||||
msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取 telegraph 页面..."), nil)
|
msg, err := ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonInfoFetchingTelegraphPage, nil)), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to reply to update: %s", err)
|
logger.Errorf("Failed to reply to update: %s", err)
|
||||||
return nil, nil, dispatcher.EndGroups
|
return nil, nil, dispatcher.EndGroups
|
||||||
@@ -193,7 +203,9 @@ func GetTphPicsFromMessageWithReply(ctx *ext.Context, update *ext.Update) (*type
|
|||||||
page, err := tphutil.DefaultClient().GetPage(ctx, pagepath)
|
page, err := tphutil.DefaultClient().GetPage(ctx, pagepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to get telegraph page: %s", err)
|
logger.Errorf("Failed to get telegraph page: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取 telegraph 页面失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetTelegraphPageFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
})), nil)
|
||||||
return nil, nil, dispatcher.EndGroups
|
return nil, nil, dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
imgs := make([]string, 0)
|
imgs := make([]string, 0)
|
||||||
@@ -227,7 +239,7 @@ func GetTphPicsFromMessageWithReply(ctx *ext.Context, update *ext.Update) (*type
|
|||||||
}
|
}
|
||||||
if len(imgs) == 0 {
|
if len(imgs) == 0 {
|
||||||
logger.Warn("No images found in telegraph page")
|
logger.Warn("No images found in telegraph page")
|
||||||
ctx.Reply(update, ext.ReplyTextString("在 telegraph 页面中未找到图片"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorNoImagesInTelegraphPage, nil)), nil)
|
||||||
return nil, nil, dispatcher.EndGroups
|
return nil, nil, dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
return msg, &TelegraphResult{
|
return msg, &TelegraphResult{
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import (
|
|||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/core"
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
"github.com/krau/SaveAny-Bot/core/tasks/parsed"
|
parsed "github.com/krau/SaveAny-Bot/core/tasks/parsed"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/parser"
|
"github.com/krau/SaveAny-Bot/pkg/parser"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
@@ -21,7 +23,9 @@ func CreateAndAddParsedTaskWithEdit(ctx *ext.Context, stor storage.Storage, dirP
|
|||||||
log.FromContext(ctx).Errorf("Failed to add task: %s", err)
|
log.FromContext(ctx).Errorf("Failed to add task: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: msgID,
|
ID: msgID,
|
||||||
Message: "任务添加失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorTaskAddFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package shortcut
|
package shortcut
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -11,6 +10,8 @@ import (
|
|||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/ruleutil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/ruleutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/core"
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
"github.com/krau/SaveAny-Bot/core/tasks/batchtfile"
|
"github.com/krau/SaveAny-Bot/core/tasks/batchtfile"
|
||||||
@@ -29,7 +30,9 @@ func CreateAndAddTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor storage
|
|||||||
logger.Errorf("Failed to get user by chat ID: %s", err)
|
logger.Errorf("Failed to get user by chat ID: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: "获取用户失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorGetUserWithErrFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -47,7 +50,9 @@ func CreateAndAddTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor storage
|
|||||||
logger.Errorf("Failed to get storage by user ID and name: %s", err)
|
logger.Errorf("Failed to get storage by user ID and name: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: "获取存储失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorGetStorageFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -65,7 +70,9 @@ startCreateTask:
|
|||||||
logger.Errorf("create task failed: %s", err)
|
logger.Errorf("create task failed: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: "创建任务失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorTaskCreateFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -73,7 +80,9 @@ startCreateTask:
|
|||||||
logger.Errorf("add task failed: %s", err)
|
logger.Errorf("add task failed: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: "添加任务失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorTaskAddFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -95,7 +104,9 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
logger.Errorf("Failed to get user by chat ID: %s", err)
|
logger.Errorf("Failed to get user by chat ID: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: "获取用户失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorGetUserWithErrFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -132,7 +143,9 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
logger.Errorf("Failed to get storage by user ID and name: %s", err)
|
logger.Errorf("Failed to get storage by user ID and name: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: "获取存储失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorGetStorageFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -144,7 +157,9 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
logger.Errorf("Failed to create task element: %s", err)
|
logger.Errorf("Failed to create task element: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: "任务创建失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorTaskCreateFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -179,7 +194,9 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
logger.Errorf("Failed to create task element for album file: %s", err)
|
logger.Errorf("Failed to create task element for album file: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: "任务创建失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorTaskCreateFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
@@ -194,13 +211,17 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
logger.Errorf("Failed to add batch task: %s", err)
|
logger.Errorf("Failed to add batch task: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: "批量任务添加失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorTaskAddFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: fmt.Sprintf("已添加批量任务, 共 %d 个文件", len(files)),
|
Message: i18n.T(i18nk.BotMsgCommonInfoBatchTasksAdded, map[string]any{
|
||||||
|
"Count": len(files),
|
||||||
|
}),
|
||||||
ReplyMarkup: nil,
|
ReplyMarkup: nil,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tphutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tphutil"
|
||||||
"github.com/krau/SaveAny-Bot/core"
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
@@ -38,7 +40,9 @@ func CreateAndAddtelegraphWithEdit(
|
|||||||
log.FromContext(ctx).Errorf("Failed to add task: %s", err)
|
log.FromContext(ctx).Errorf("Failed to add task: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: "任务添加失败: " + err.Error(),
|
Message: i18n.T(i18nk.BotMsgCommonErrorTaskAddFailed, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,18 +34,18 @@ func handleWatchCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
userChatID := update.GetUserChat().GetID()
|
userChatID := update.GetUserChat().GetID()
|
||||||
user, err := database.GetUserByChatID(ctx, userChatID)
|
user, err := database.GetUserByChatID(ctx, userChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("获取用户失败: %s", err)
|
logger.Errorf("Failed to get user: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetUserFailed)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if user.DefaultStorage == "" {
|
if user.DefaultStorage == "" {
|
||||||
ctx.Reply(update, ext.ReplyTextString("请先设置默认存储, 使用 /storage 命令"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorDefaultStorageNotSet)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
chatArg := args[1]
|
chatArg := args[1]
|
||||||
chatID, err := tgutil.ParseChatID(ctx, chatArg)
|
chatID, err := tgutil.ParseChatID(ctx, chatArg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("无效的ID或用户名: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorInvalidIdOrUsername, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
watching, err := user.WatchingChat(ctx, chatID)
|
watching, err := user.WatchingChat(ctx, chatID)
|
||||||
@@ -54,7 +54,7 @@ func handleWatchCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if watching {
|
if watching {
|
||||||
ctx.Reply(update, ext.ReplyTextString("已经在监听此聊天"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchInfoAlreadyWatchingChat)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
filter := ""
|
filter := ""
|
||||||
@@ -63,19 +63,19 @@ func handleWatchCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
filterType := strings.Split(filterArg, ":")[0]
|
filterType := strings.Split(filterArg, ":")[0]
|
||||||
filterData := strings.Split(filterArg, ":")[1]
|
filterData := strings.Split(filterArg, ":")[1]
|
||||||
if filterType == "" || filterData == "" {
|
if filterType == "" || filterData == "" {
|
||||||
ctx.Reply(update, ext.ReplyTextString("过滤器格式错误, 请使用 <过滤器类型>:<表达式>"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchErrorFilterFormatInvalid)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
switch filterType {
|
switch filterType {
|
||||||
case "msgre":
|
case "msgre":
|
||||||
_, err := regexp.Compile(filterData)
|
_, err := regexp.Compile(filterData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("正则表达式格式错误: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorInvalidRegex, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
filter = filterType + ":" + filterData
|
filter = filterType + ":" + filterData
|
||||||
default:
|
default:
|
||||||
ctx.Reply(update, ext.ReplyTextString("不支持的过滤器类型, 请参阅文档"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchErrorFilterTypeUnsupported)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,10 +85,10 @@ func handleWatchCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
Filter: filter,
|
Filter: filter,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.Errorf("Failed to watch chat %d: %s", chatID, err)
|
logger.Errorf("Failed to watch chat %d: %s", chatID, err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("监听聊天失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchErrorWatchChatFailed, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString("已开始监听聊天: "+chatArg), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchInfoWatchChatStarted, map[string]any{"Chat": chatArg})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,22 +97,22 @@ func handleLswatchCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
userChatID := update.GetUserChat().GetID()
|
userChatID := update.GetUserChat().GetID()
|
||||||
user, err := database.GetUserByChatID(ctx, userChatID)
|
user, err := database.GetUserByChatID(ctx, userChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("获取用户失败: %s", err)
|
logger.Errorf("Failed to get user: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetUserFailed)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
chats := user.WatchChats
|
chats := user.WatchChats
|
||||||
if len(chats) == 0 {
|
if len(chats) == 0 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("当前没有监听任何聊天"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchInfoWatchListEmpty)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("当前监听的聊天:\n")
|
sb.WriteString(i18n.T(i18nk.BotMsgWatchInfoWatchListHeader))
|
||||||
for _, chat := range chats {
|
for _, chat := range chats {
|
||||||
sb.WriteString("- ")
|
sb.WriteString("- ")
|
||||||
sb.WriteString(fmt.Sprintf("%d", chat.ChatID))
|
sb.WriteString(fmt.Sprintf("%d", chat.ChatID))
|
||||||
if chat.Filter != "" {
|
if chat.Filter != "" {
|
||||||
sb.WriteString(" (过滤器: ")
|
sb.WriteString(i18n.T(i18nk.BotMsgWatchInfoWatchListFilterPrefix))
|
||||||
sb.WriteString(chat.Filter)
|
sb.WriteString(chat.Filter)
|
||||||
sb.WriteString(")")
|
sb.WriteString(")")
|
||||||
}
|
}
|
||||||
@@ -126,32 +126,35 @@ func handleUnwatchCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
args := strings.Split(update.EffectiveMessage.Text, " ")
|
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
ctx.Reply(update, ext.ReplyTextString("请提供要取消监听的聊天ID或用户名"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchErrorUnwatchNoChatProvided)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
userChatID := update.GetUserChat().GetID()
|
userChatID := update.GetUserChat().GetID()
|
||||||
user, err := database.GetUserByChatID(ctx, userChatID)
|
user, err := database.GetUserByChatID(ctx, userChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("获取用户失败: %s", err)
|
logger.Errorf("Failed to get user: %s", err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorGetUserFailed)), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
chatArg := args[1]
|
chatArg := args[1]
|
||||||
chatID, err := tgutil.ParseChatID(ctx, chatArg)
|
chatID, err := tgutil.ParseChatID(ctx, chatArg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Reply(update, ext.ReplyTextString("无效的ID或用户名: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorInvalidIdOrUsername, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
if err := user.UnwatchChat(ctx, chatID); err != nil {
|
if err := user.UnwatchChat(ctx, chatID); err != nil {
|
||||||
logger.Errorf("Failed to unwatch chat %d: %s", chatID, err)
|
logger.Errorf("Failed to unwatch chat %d: %s", chatID, err)
|
||||||
ctx.Reply(update, ext.ReplyTextString("取消监听聊天失败: "+err.Error()), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchErrorUnwatchChatFailed, map[string]any{"Error": err.Error()})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.Reply(update, ext.ReplyTextString("已取消监听聊天: "+chatArg), nil)
|
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchInfoWatchChatStopped, map[string]any{"Chat": chatArg})), nil)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
|
func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
|
||||||
|
if userclient.GetCtx() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
logger := log.FromContext(userclient.GetCtx())
|
logger := log.FromContext(userclient.GetCtx())
|
||||||
for event := range ch {
|
for event := range ch {
|
||||||
logger.Debug("Received media message event", "chat_id", event.ChatID, "file_name", event.File.Name())
|
logger.Debug("Received media message event", "chat_id", event.ChatID, "file_name", event.File.Name())
|
||||||
|
|||||||
@@ -23,23 +23,16 @@ var uc *gotgproto.Client
|
|||||||
var ectx *ext.Context
|
var ectx *ext.Context
|
||||||
|
|
||||||
func GetCtx() *ext.Context {
|
func GetCtx() *ext.Context {
|
||||||
if uc == nil {
|
|
||||||
panic("User client is not initialized, please call Login first")
|
|
||||||
}
|
|
||||||
if ectx != nil {
|
if ectx != nil {
|
||||||
return ectx
|
return ectx
|
||||||
}
|
}
|
||||||
|
if uc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
ectx = uc.CreateContext()
|
ectx = uc.CreateContext()
|
||||||
return ectx
|
return ectx
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetClient() *gotgproto.Client {
|
|
||||||
if uc == nil {
|
|
||||||
panic("User client is not initialized, please call Login first")
|
|
||||||
}
|
|
||||||
return uc
|
|
||||||
}
|
|
||||||
|
|
||||||
func Login(ctx context.Context) (*gotgproto.Client, error) {
|
func Login(ctx context.Context) (*gotgproto.Client, error) {
|
||||||
log.FromContext(ctx).Debug("Logging in user client")
|
log.FromContext(ctx).Debug("Logging in user client")
|
||||||
if uc != nil {
|
if uc != nil {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/cmd/upload"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +15,11 @@ var rootCmd = &cobra.Command{
|
|||||||
Run: Run,
|
Run: Run,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
config.RegisterFlags(rootCmd)
|
||||||
|
upload.Register(rootCmd)
|
||||||
|
}
|
||||||
|
|
||||||
func Execute(ctx context.Context) {
|
func Execute(ctx context.Context) {
|
||||||
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|||||||
42
cmd/run.go
42
cmd/run.go
@@ -14,7 +14,6 @@ import (
|
|||||||
userclient "github.com/krau/SaveAny-Bot/client/user"
|
userclient "github.com/krau/SaveAny-Bot/client/user"
|
||||||
"github.com/krau/SaveAny-Bot/common/cache"
|
"github.com/krau/SaveAny-Bot/common/cache"
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n"
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/core"
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
@@ -34,7 +33,7 @@ func Run(cmd *cobra.Command, _ []string) {
|
|||||||
})
|
})
|
||||||
ctx = log.WithContext(ctx, logger)
|
ctx = log.WithContext(ctx, logger)
|
||||||
|
|
||||||
exitChan, err := initAll(ctx)
|
exitChan, err := initAll(ctx, cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Init failed", "error", err)
|
logger.Fatal("Init failed", "error", err)
|
||||||
}
|
}
|
||||||
@@ -46,36 +45,35 @@ func Run(cmd *cobra.Command, _ []string) {
|
|||||||
core.Run(ctx)
|
core.Run(ctx)
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
logger.Info(i18n.T(i18nk.LifetimeExiting))
|
logger.Info("Exiting...")
|
||||||
defer logger.Info(i18n.T(i18nk.LifetimeBye))
|
defer logger.Info("Exit complete")
|
||||||
cleanCache()
|
cleanCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initAll(ctx context.Context) (<-chan struct{}, error) {
|
func initAll(ctx context.Context, cmd *cobra.Command) (<-chan struct{}, error) {
|
||||||
if err := config.Init(ctx); err != nil {
|
configFile := config.GetConfigFile(cmd)
|
||||||
|
if err := config.Init(ctx, configFile); err != nil {
|
||||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||||
}
|
}
|
||||||
cache.Init()
|
cache.Init()
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
i18n.Init(config.C().Lang)
|
i18n.Init(config.C().Lang)
|
||||||
logger.Info(i18n.T(i18nk.LifetimeIniting))
|
logger.Info("Initializing...")
|
||||||
database.Init(ctx)
|
database.Init(ctx)
|
||||||
storage.LoadStorages(ctx)
|
storage.LoadStorages(ctx)
|
||||||
if config.C().Parser.PluginEnable {
|
if config.C().Parser.PluginEnable {
|
||||||
for _, dir := range config.C().Parser.PluginDirs {
|
for _, dir := range config.C().Parser.PluginDirs {
|
||||||
if err := parsers.LoadPlugins(ctx, dir); err != nil {
|
if err := parsers.LoadPlugins(ctx, dir); err != nil {
|
||||||
logger.Error(i18n.T(i18nk.ParserPluginLoadFailed), "dir", dir, "error", err)
|
logger.Error("Failed to load parser plugins", "dir", dir, "error", err)
|
||||||
} else {
|
} else {
|
||||||
logger.Debug(i18n.T(i18nk.ParserPluginLoadedDir), "dir", dir)
|
logger.Debug("Loaded parser plugins from directory", "dir", dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config.C().Telegram.Userbot.Enable {
|
if config.C().Telegram.Userbot.Enable {
|
||||||
_, err := userclient.Login(ctx)
|
_, err := userclient.Login(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal(i18n.T(i18nk.LifetimeUserLoginFailed, map[string]any{
|
logger.Fatal("User login failed", "error", err)
|
||||||
"Error": err,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bot.Init(ctx), nil
|
return bot.Init(ctx), nil
|
||||||
@@ -87,33 +85,23 @@ func cleanCache() {
|
|||||||
}
|
}
|
||||||
if config.C().Temp.BasePath != "" && !config.C().Stream {
|
if config.C().Temp.BasePath != "" && !config.C().Stream {
|
||||||
if slices.Contains([]string{"/", ".", "\\", ".."}, filepath.Clean(config.C().Temp.BasePath)) {
|
if slices.Contains([]string{"/", ".", "\\", ".."}, filepath.Clean(config.C().Temp.BasePath)) {
|
||||||
log.Error(i18n.T(i18nk.ConfigErrInvalidCacheDir, map[string]any{
|
log.Error("Invalid cache directory", "path", config.C().Temp.BasePath)
|
||||||
"Path": config.C().Temp.BasePath,
|
|
||||||
}))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentDir, err := os.Getwd()
|
currentDir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(i18n.T(i18nk.ErrGetWorkdirFailed, map[string]any{
|
log.Error("Failed to get working directory", "error", err)
|
||||||
"Error": err,
|
|
||||||
}))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cachePath := filepath.Join(currentDir, config.C().Temp.BasePath)
|
cachePath := filepath.Join(currentDir, config.C().Temp.BasePath)
|
||||||
cachePath, err = filepath.Abs(cachePath)
|
cachePath, err = filepath.Abs(cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(i18n.T(i18nk.ErrGetCacheAbsPathFailed, map[string]any{
|
log.Error("Failed to get absolute cache path", "error", err)
|
||||||
"Error": err,
|
|
||||||
}))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Info(i18n.T(i18nk.LifetimeCleaningCache, map[string]any{
|
log.Info("Cleaning cache directory", "path", cachePath)
|
||||||
"Path": cachePath,
|
|
||||||
}))
|
|
||||||
if err := fsutil.RemoveAllInDir(cachePath); err != nil {
|
if err := fsutil.RemoveAllInDir(cachePath); err != nil {
|
||||||
log.Error(i18n.T(i18nk.ErrCleanCacheFailed, map[string]any{
|
log.Error("Failed to clean cache directory", "error", err)
|
||||||
"Error": err,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
128
cmd/upload/cmd.go
Normal file
128
cmd/upload/cmd.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package upload
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/cache"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/ioutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||||
|
stortype "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var uploadCmd = &cobra.Command{
|
||||||
|
Use: "upload",
|
||||||
|
Short: "upload local files to storage",
|
||||||
|
RunE: Upload,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Register(root *cobra.Command) {
|
||||||
|
uploadCmd.Flags().StringP("file", "f", "", "file path to upload")
|
||||||
|
uploadCmd.MarkFlagRequired("file")
|
||||||
|
uploadCmd.Flags().StringP("storage", "s", "", "storage name to upload to")
|
||||||
|
uploadCmd.MarkFlagRequired("storage")
|
||||||
|
uploadCmd.Flags().StringP("dir", "d", "", "storage dir to upload to, default is the base_path of the storage")
|
||||||
|
uploadCmd.Flags().Bool("no-progress", false, "disable progress bar")
|
||||||
|
root.AddCommand(uploadCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Upload(cmd *cobra.Command, args []string) error {
|
||||||
|
storname, err := cmd.Flags().GetString("storage")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fp, err := cmd.Flags().GetString("file")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dirPath, err := cmd.Flags().GetString("dir")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
noProgress, err := cmd.Flags().GetBool("no-progress")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := cmd.Context()
|
||||||
|
log := log.FromContext(ctx)
|
||||||
|
configFile := config.GetConfigFile(cmd)
|
||||||
|
if err := config.Init(ctx, configFile); err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
cache.Init()
|
||||||
|
database.Init(ctx)
|
||||||
|
|
||||||
|
stor, err := storage.GetStorageByName(ctx, storname)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to get storage", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch stor.Type() {
|
||||||
|
case stortype.Telegram:
|
||||||
|
bot.Init(ctx)
|
||||||
|
default:
|
||||||
|
// placeholder for other storage types that may need special initialization
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filepath.Clean(fp))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to open file", "error", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
fileInfo, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to get file info", "error", err)
|
||||||
|
}
|
||||||
|
fileName := fileInfo.Name()
|
||||||
|
fileSize := fileInfo.Size()
|
||||||
|
|
||||||
|
uploadPath := stor.JoinStoragePath(path.Join(dirPath, fileName))
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, ctxkey.ContentLength, fileSize)
|
||||||
|
ctx = tgutil.ExtWithContext(ctx, bot.ExtContext())
|
||||||
|
|
||||||
|
// Create progress reader and UI
|
||||||
|
var reader io.Reader
|
||||||
|
var progressUI *UploadProgress
|
||||||
|
log.Info("Uploading file...", "file", fp, "to", storname, "as", uploadPath)
|
||||||
|
|
||||||
|
if !noProgress && fileSize > 0 {
|
||||||
|
progressUI = NewUploadProgress(ctx, fileName, fileSize)
|
||||||
|
progressUI.Start()
|
||||||
|
|
||||||
|
reader = ioutil.NewProgressReader(file, fileSize, func(read int64, total int64) {
|
||||||
|
if total > 0 {
|
||||||
|
progressUI.UpdateProgress(float64(read) / float64(total))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
reader = file
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stor.Save(ctx, reader, uploadPath); err != nil {
|
||||||
|
if progressUI != nil {
|
||||||
|
progressUI.SetError(err)
|
||||||
|
progressUI.Wait()
|
||||||
|
}
|
||||||
|
log.Fatal("Failed to upload file", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if progressUI != nil {
|
||||||
|
progressUI.Done()
|
||||||
|
progressUI.Wait()
|
||||||
|
}
|
||||||
|
log.Info("File uploaded successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
35
cmd/upload/progress_stub.go
Normal file
35
cmd/upload/progress_stub.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//go:build no_bubbletea
|
||||||
|
|
||||||
|
package upload
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type uploadModel struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadProgress manages the progress UI for uploads
|
||||||
|
type UploadProgress struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUploadProgress creates a new upload progress tracker
|
||||||
|
func NewUploadProgress(ctx context.Context, fileName string, fileSize int64) *UploadProgress {
|
||||||
|
return &UploadProgress{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the progress UI in a goroutine and returns immediately
|
||||||
|
func (up *UploadProgress) Start() {}
|
||||||
|
|
||||||
|
// UpdateProgress updates the progress bar with a new percentage (0.0 - 1.0)
|
||||||
|
func (up *UploadProgress) UpdateProgress(percent float64) {}
|
||||||
|
|
||||||
|
// SetError sets an error and quits the progress UI
|
||||||
|
func (up *UploadProgress) SetError(err error) {}
|
||||||
|
|
||||||
|
// Done signals that the upload is complete
|
||||||
|
func (up *UploadProgress) Done() {}
|
||||||
|
|
||||||
|
// Wait waits for the progress UI to finish
|
||||||
|
func (up *UploadProgress) Wait() {}
|
||||||
|
|
||||||
|
// Quit quits the progress UI
|
||||||
|
func (up *UploadProgress) Quit() {}
|
||||||
178
cmd/upload/progress_tea.go
Normal file
178
cmd/upload/progress_tea.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
//go:build !no_bubbletea
|
||||||
|
|
||||||
|
package upload
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/progress"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262"))
|
||||||
|
)
|
||||||
|
|
||||||
|
// progressMsg is sent to update the progress bar
|
||||||
|
type progressMsg float64
|
||||||
|
|
||||||
|
// progressErrMsg is sent when an error occurs
|
||||||
|
type progressErrMsg struct{ err error }
|
||||||
|
|
||||||
|
// progressDoneMsg is sent when the upload is complete
|
||||||
|
type progressDoneMsg struct{}
|
||||||
|
|
||||||
|
// uploadModel is the bubbletea model for the upload progress UI
|
||||||
|
type uploadModel struct {
|
||||||
|
progress progress.Model
|
||||||
|
fileName string
|
||||||
|
fileSize int64
|
||||||
|
bytesRead int64
|
||||||
|
err error
|
||||||
|
done bool
|
||||||
|
quitting bool
|
||||||
|
width int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUploadModel(fileName string, fileSize int64) uploadModel {
|
||||||
|
p := progress.New(
|
||||||
|
progress.WithDefaultGradient(),
|
||||||
|
progress.WithWidth(50),
|
||||||
|
)
|
||||||
|
return uploadModel{
|
||||||
|
progress: p,
|
||||||
|
fileName: fileName,
|
||||||
|
fileSize: fileSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uploadModel) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uploadModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.progress.Width = min(msg.Width-10, 80)
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case progressMsg:
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
percent := float64(msg)
|
||||||
|
m.bytesRead = int64(percent * float64(m.fileSize))
|
||||||
|
|
||||||
|
cmds = append(cmds, m.progress.SetPercent(percent))
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
|
||||||
|
case progressErrMsg:
|
||||||
|
m.err = msg.err
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case progressDoneMsg:
|
||||||
|
m.done = true
|
||||||
|
m.progress.SetPercent(1.0)
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case progress.FrameMsg:
|
||||||
|
// Don't process frame messages if we're done or quitting
|
||||||
|
if m.done || m.quitting {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
progressModel, cmd := m.progress.Update(msg)
|
||||||
|
m.progress = progressModel.(progress.Model)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uploadModel) View() string {
|
||||||
|
if m.err != nil {
|
||||||
|
return fmt.Sprintf("\n ❌ Error: %s\n\n", m.err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("\n")
|
||||||
|
|
||||||
|
// File info
|
||||||
|
sb.WriteString(fmt.Sprintf(" 📁 %s\n", m.fileName))
|
||||||
|
sb.WriteString(fmt.Sprintf(" 📊 %s / %s\n\n",
|
||||||
|
humanize.Bytes(uint64(m.bytesRead)),
|
||||||
|
humanize.Bytes(uint64(m.fileSize)),
|
||||||
|
))
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
sb.WriteString(" ")
|
||||||
|
sb.WriteString(m.progress.View())
|
||||||
|
sb.WriteString("\n\n")
|
||||||
|
|
||||||
|
if m.done {
|
||||||
|
sb.WriteString(" √ Upload complete!\n\n")
|
||||||
|
} else {
|
||||||
|
sb.WriteString(helpStyle.Render(" Press Ctrl+C to cancel"))
|
||||||
|
sb.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadProgress manages the progress UI for uploads
|
||||||
|
type UploadProgress struct {
|
||||||
|
program *tea.Program
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUploadProgress creates a new upload progress tracker
|
||||||
|
func NewUploadProgress(ctx context.Context, fileName string, fileSize int64) *UploadProgress {
|
||||||
|
model := newUploadModel(fileName, fileSize)
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
p := tea.NewProgram(
|
||||||
|
model,
|
||||||
|
tea.WithoutSignalHandler(),
|
||||||
|
tea.WithContext(ctx),
|
||||||
|
tea.WithInput(nil), // Disable keyboard input, rely on context cancellation
|
||||||
|
)
|
||||||
|
return &UploadProgress{
|
||||||
|
program: p,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the progress UI in a goroutine and returns immediately
|
||||||
|
func (up *UploadProgress) Start() {
|
||||||
|
go func() {
|
||||||
|
up.program.Run()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProgress updates the progress bar with a new percentage (0.0 - 1.0)
|
||||||
|
func (up *UploadProgress) UpdateProgress(percent float64) {
|
||||||
|
up.program.Send(progressMsg(percent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetError sets an error and quits the progress UI
|
||||||
|
func (up *UploadProgress) SetError(err error) {
|
||||||
|
up.program.Send(progressErrMsg{err: err})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done signals that the upload is complete
|
||||||
|
func (up *UploadProgress) Done() {
|
||||||
|
up.program.Send(progressDoneMsg{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits for the progress UI to finish
|
||||||
|
func (up *UploadProgress) Wait() {
|
||||||
|
up.program.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit quits the progress UI
|
||||||
|
func (up *UploadProgress) Quit() {
|
||||||
|
up.program.Quit()
|
||||||
|
}
|
||||||
@@ -4,12 +4,221 @@ package i18nk
|
|||||||
type Key string
|
type Key string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
BotMsgCancelErrorCancelFailed Key = "bot.msg.cancel.error_cancel_failed"
|
||||||
|
BotMsgCancelInfoCancelRequested Key = "bot.msg.cancel.info_cancel_requested"
|
||||||
|
BotMsgCancelInfoCancellingTask Key = "bot.msg.cancel.info_cancelling_task"
|
||||||
|
BotMsgCancelUsage Key = "bot.msg.cancel.usage"
|
||||||
|
BotMsgCmdCancel Key = "bot.msg.cmd.cancel"
|
||||||
|
BotMsgCmdConfig Key = "bot.msg.cmd.config"
|
||||||
|
BotMsgCmdDir Key = "bot.msg.cmd.dir"
|
||||||
|
BotMsgCmdDl Key = "bot.msg.cmd.dl"
|
||||||
|
BotMsgCmdFnametmpl Key = "bot.msg.cmd.fnametmpl"
|
||||||
|
BotMsgCmdHelp Key = "bot.msg.cmd.help"
|
||||||
|
BotMsgCmdLswatch Key = "bot.msg.cmd.lswatch"
|
||||||
|
BotMsgCmdParser Key = "bot.msg.cmd.parser"
|
||||||
|
BotMsgCmdRule Key = "bot.msg.cmd.rule"
|
||||||
|
BotMsgCmdSave Key = "bot.msg.cmd.save"
|
||||||
|
BotMsgCmdSilent Key = "bot.msg.cmd.silent"
|
||||||
|
BotMsgCmdStart Key = "bot.msg.cmd.start"
|
||||||
|
BotMsgCmdStorage Key = "bot.msg.cmd.storage"
|
||||||
|
BotMsgCmdTask Key = "bot.msg.cmd.task"
|
||||||
|
BotMsgCmdUnwatch Key = "bot.msg.cmd.unwatch"
|
||||||
|
BotMsgCmdUpdate Key = "bot.msg.cmd.update"
|
||||||
|
BotMsgCmdWatch Key = "bot.msg.cmd.watch"
|
||||||
|
BotMsgCommonErrorBuildDirSelectKeyboardFailed Key = "bot.msg.common.error_build_dir_select_keyboard_failed"
|
||||||
|
BotMsgCommonErrorBuildStorageSelectKeyboardFailed Key = "bot.msg.common.error_build_storage_select_keyboard_failed"
|
||||||
|
BotMsgCommonErrorBuildStorageSelectMessageFailed Key = "bot.msg.common.error_build_storage_select_message_failed"
|
||||||
|
BotMsgCommonErrorDataExpired Key = "bot.msg.common.error_data_expired"
|
||||||
|
BotMsgCommonErrorDefaultStorageNotSet Key = "bot.msg.common.error_default_storage_not_set"
|
||||||
|
BotMsgCommonErrorGetDirFailed Key = "bot.msg.common.error_get_dir_failed"
|
||||||
|
BotMsgCommonErrorGetFileFailed Key = "bot.msg.common.error_get_file_failed"
|
||||||
|
BotMsgCommonErrorGetMessagesFailed Key = "bot.msg.common.error_get_messages_failed"
|
||||||
|
BotMsgCommonErrorGetStorageFailed Key = "bot.msg.common.error_get_storage_failed"
|
||||||
|
BotMsgCommonErrorGetTelegraphPageFailed Key = "bot.msg.common.error_get_telegraph_page_failed"
|
||||||
|
BotMsgCommonErrorGetUserFailed Key = "bot.msg.common.error_get_user_failed"
|
||||||
|
BotMsgCommonErrorGetUserInfoFailed Key = "bot.msg.common.error_get_user_info_failed"
|
||||||
|
BotMsgCommonErrorGetUserWithErrFailed Key = "bot.msg.common.error_get_user_with_err_failed"
|
||||||
|
BotMsgCommonErrorInvalidIdOrUsername Key = "bot.msg.common.error_invalid_id_or_username"
|
||||||
|
BotMsgCommonErrorInvalidMsgIdRange Key = "bot.msg.common.error_invalid_msg_id_range"
|
||||||
|
BotMsgCommonErrorInvalidRegex Key = "bot.msg.common.error_invalid_regex"
|
||||||
|
BotMsgCommonErrorNoAvailableStorage Key = "bot.msg.common.error_no_available_storage"
|
||||||
|
BotMsgCommonErrorNoImagesInTelegraphPage Key = "bot.msg.common.error_no_images_in_telegraph_page"
|
||||||
|
BotMsgCommonErrorNoMessagesInRange Key = "bot.msg.common.error_no_messages_in_range"
|
||||||
|
BotMsgCommonErrorNoPermission Key = "bot.msg.common.error_no_permission"
|
||||||
|
BotMsgCommonErrorNoSavableFilesFound Key = "bot.msg.common.error_no_savable_files_found"
|
||||||
|
BotMsgCommonErrorNoSavableMessagesInRange Key = "bot.msg.common.error_no_savable_messages_in_range"
|
||||||
|
BotMsgCommonErrorParseTelegraphPathFailed Key = "bot.msg.common.error_parse_telegraph_path_failed"
|
||||||
|
BotMsgCommonErrorTaskAddFailed Key = "bot.msg.common.error_task_add_failed"
|
||||||
|
BotMsgCommonErrorTaskCreateFailed Key = "bot.msg.common.error_task_create_failed"
|
||||||
|
BotMsgCommonErrorUpdateUserInfoFailed Key = "bot.msg.common.error_update_user_info_failed"
|
||||||
|
BotMsgCommonInfoBatchTasksAdded Key = "bot.msg.common.info_batch_tasks_added"
|
||||||
|
BotMsgCommonInfoDefaultStorageSet Key = "bot.msg.common.info_default_storage_set"
|
||||||
|
BotMsgCommonInfoDefaultStorageWithDirSet Key = "bot.msg.common.info_default_storage_with_dir_set"
|
||||||
|
BotMsgCommonInfoFetchingFileInfo Key = "bot.msg.common.info_fetching_file_info"
|
||||||
|
BotMsgCommonInfoFetchingMessages Key = "bot.msg.common.info_fetching_messages"
|
||||||
|
BotMsgCommonInfoFetchingTelegraphPage Key = "bot.msg.common.info_fetching_telegraph_page"
|
||||||
|
BotMsgCommonInfoFoundFilesSelectStorage Key = "bot.msg.common.info_found_files_select_storage"
|
||||||
|
BotMsgCommonInfoSilentModeOff Key = "bot.msg.common.info_silent_mode_off"
|
||||||
|
BotMsgCommonInfoSilentModeOn Key = "bot.msg.common.info_silent_mode_on"
|
||||||
|
BotMsgCommonInfoTaskAdded Key = "bot.msg.common.info_task_added"
|
||||||
|
BotMsgCommonPromptSelectDefaultDir Key = "bot.msg.common.prompt_select_default_dir"
|
||||||
|
BotMsgCommonPromptSelectDefaultStorage Key = "bot.msg.common.prompt_select_default_storage"
|
||||||
|
BotMsgCommonPromptSelectDir Key = "bot.msg.common.prompt_select_dir"
|
||||||
|
BotMsgConfigButtonFilenameStrategy Key = "bot.msg.config.button_filename_strategy"
|
||||||
|
BotMsgConfigErrorInvalidCallbackData Key = "bot.msg.config.error_invalid_callback_data"
|
||||||
|
BotMsgConfigErrorInvalidTemplate Key = "bot.msg.config.error_invalid_template"
|
||||||
|
BotMsgConfigFnametmplHelp Key = "bot.msg.config.fnametmpl_help"
|
||||||
|
BotMsgConfigInfoCurrentTemplatePrefix Key = "bot.msg.config.info_current_template_prefix"
|
||||||
|
BotMsgConfigInfoFilenameStrategySet Key = "bot.msg.config.info_filename_strategy_set"
|
||||||
|
BotMsgConfigInfoTemplateUpdated Key = "bot.msg.config.info_template_updated"
|
||||||
|
BotMsgConfigPromptSelectFilenameStrategy Key = "bot.msg.config.prompt_select_filename_strategy"
|
||||||
|
BotMsgConfigPromptSelectOption Key = "bot.msg.config.prompt_select_option"
|
||||||
|
BotMsgDirButtonDefault Key = "bot.msg.dir.button_default"
|
||||||
|
BotMsgDirErrorCreateDirFailed Key = "bot.msg.dir.error_create_dir_failed"
|
||||||
|
BotMsgDirErrorDeleteDirFailed Key = "bot.msg.dir.error_delete_dir_failed"
|
||||||
|
BotMsgDirErrorGetUserDirsFailed Key = "bot.msg.dir.error_get_user_dirs_failed"
|
||||||
|
BotMsgDirErrorGetUserFailed Key = "bot.msg.dir.error_get_user_failed"
|
||||||
|
BotMsgDirErrorInvalidDirId Key = "bot.msg.dir.error_invalid_dir_id"
|
||||||
|
BotMsgDirErrorUnknownOperation Key = "bot.msg.dir.error_unknown_operation"
|
||||||
|
BotMsgDirHelpAddExampleCmd Key = "bot.msg.dir.help_add_example_cmd"
|
||||||
|
BotMsgDirHelpAddExamplePrefix Key = "bot.msg.dir.help_add_example_prefix"
|
||||||
|
BotMsgDirHelpAddSuffix Key = "bot.msg.dir.help_add_suffix"
|
||||||
|
BotMsgDirHelpAvailableOps Key = "bot.msg.dir.help_available_ops"
|
||||||
|
BotMsgDirHelpDelExampleCmd Key = "bot.msg.dir.help_del_example_cmd"
|
||||||
|
BotMsgDirHelpDelExamplePrefix Key = "bot.msg.dir.help_del_example_prefix"
|
||||||
|
BotMsgDirHelpDelSuffix Key = "bot.msg.dir.help_del_suffix"
|
||||||
|
BotMsgDirHelpExistingDirsPrefix Key = "bot.msg.dir.help_existing_dirs_prefix"
|
||||||
|
BotMsgDirHelpUsage Key = "bot.msg.dir.help_usage"
|
||||||
|
BotMsgDirInfoCreateDirSuccess Key = "bot.msg.dir.info_create_dir_success"
|
||||||
|
BotMsgDirInfoDeleteDirSuccess Key = "bot.msg.dir.info_delete_dir_success"
|
||||||
|
BotMsgDlErrorNoValidLinks Key = "bot.msg.dl.error_no_valid_links"
|
||||||
|
BotMsgDlInfoFilesSelectStorage Key = "bot.msg.dl.info_files_select_storage"
|
||||||
|
BotMsgDlUsage Key = "bot.msg.dl.usage"
|
||||||
BotMsgHelpTextFmt Key = "bot.msg.help_text_fmt"
|
BotMsgHelpTextFmt Key = "bot.msg.help_text_fmt"
|
||||||
|
BotMsgMediaGroupErrorBuildStorageSelectKeyboardFailed Key = "bot.msg.media_group.error_build_storage_select_keyboard_failed"
|
||||||
|
BotMsgMediaGroupInfoGroupFoundFilesSelectStorage Key = "bot.msg.media_group.info_group_found_files_select_storage"
|
||||||
|
BotMsgMediaGroupInfoSavingFiles Key = "bot.msg.media_group.info_saving_files"
|
||||||
|
BotMsgParseErrorBuildParsedTextEntityFailed Key = "bot.msg.parse.error_build_parsed_text_entity_failed"
|
||||||
|
BotMsgParseErrorBuildStorageSelectKeyboardFailed Key = "bot.msg.parse.error_build_storage_select_keyboard_failed"
|
||||||
|
BotMsgParseErrorParseTextFailed Key = "bot.msg.parse.error_parse_text_failed"
|
||||||
|
BotMsgParseInfoAuthorPrefix Key = "bot.msg.parse.info_author_prefix"
|
||||||
|
BotMsgParseInfoDescriptionPrefix Key = "bot.msg.parse.info_description_prefix"
|
||||||
|
BotMsgParseInfoFileCountPrefix Key = "bot.msg.parse.info_file_count_prefix"
|
||||||
|
BotMsgParseInfoLinkPrefix Key = "bot.msg.parse.info_link_prefix"
|
||||||
|
BotMsgParseInfoParsing Key = "bot.msg.parse.info_parsing"
|
||||||
|
BotMsgParseInfoPromptSelectStorage Key = "bot.msg.parse.info_prompt_select_storage"
|
||||||
|
BotMsgParseInfoTotalSizePrefix Key = "bot.msg.parse.info_total_size_prefix"
|
||||||
|
BotMsgParserErrorDownloadFileFailed Key = "bot.msg.parser.error_download_file_failed"
|
||||||
|
BotMsgParserErrorFileTooLarge Key = "bot.msg.parser.error_file_too_large"
|
||||||
|
BotMsgParserErrorGetFilenameFailed Key = "bot.msg.parser.error_get_filename_failed"
|
||||||
|
BotMsgParserErrorInstallPluginFailed Key = "bot.msg.parser.error_install_plugin_failed"
|
||||||
|
BotMsgParserErrorNoValidFileInReply Key = "bot.msg.parser.error_no_valid_file_in_reply"
|
||||||
|
BotMsgParserErrorOnlyJsSupported Key = "bot.msg.parser.error_only_js_supported"
|
||||||
|
BotMsgParserErrorWrongFileType Key = "bot.msg.parser.error_wrong_file_type"
|
||||||
|
BotMsgParserHelpText Key = "bot.msg.parser.help_text"
|
||||||
|
BotMsgParserInfoInstallPluginSuccess Key = "bot.msg.parser.info_install_plugin_success"
|
||||||
|
BotMsgParserPluginNotEnabled Key = "bot.msg.parser.plugin_not_enabled"
|
||||||
|
BotMsgParserPromptReplyWithParserFile Key = "bot.msg.parser.prompt_reply_with_parser_file"
|
||||||
|
BotMsgProgressAvgSpeedPrefix Key = "bot.msg.progress.avg_speed_prefix"
|
||||||
|
BotMsgProgressBatchDonePrefix Key = "bot.msg.progress.batch_done_prefix"
|
||||||
|
BotMsgProgressBatchProcessingPrefix Key = "bot.msg.progress.batch_processing_prefix"
|
||||||
|
BotMsgProgressBatchStartPrefix Key = "bot.msg.progress.batch_start_prefix"
|
||||||
|
BotMsgProgressCurrentProgressPrefix Key = "bot.msg.progress.current_progress_prefix"
|
||||||
|
BotMsgProgressDirectDonePrefix Key = "bot.msg.progress.direct_done_prefix"
|
||||||
|
BotMsgProgressDirectStart Key = "bot.msg.progress.direct_start"
|
||||||
|
BotMsgProgressDownloadDonePrefix Key = "bot.msg.progress.download_done_prefix"
|
||||||
|
BotMsgProgressDownloadFailedPrefix Key = "bot.msg.progress.download_failed_prefix"
|
||||||
|
BotMsgProgressDownloadingPrefix Key = "bot.msg.progress.downloading_prefix"
|
||||||
|
BotMsgProgressErrorPrefix Key = "bot.msg.progress.error_prefix"
|
||||||
|
BotMsgProgressFileNamePrefix Key = "bot.msg.progress.file_name_prefix"
|
||||||
|
BotMsgProgressFileProcessingPrefix Key = "bot.msg.progress.file_processing_prefix"
|
||||||
|
BotMsgProgressFileSizePrefix Key = "bot.msg.progress.file_size_prefix"
|
||||||
|
BotMsgProgressFileStartPrefix Key = "bot.msg.progress.file_start_prefix"
|
||||||
|
BotMsgProgressParsedDonePrefix Key = "bot.msg.progress.parsed_done_prefix"
|
||||||
|
BotMsgProgressParsedStartPrefix Key = "bot.msg.progress.parsed_start_prefix"
|
||||||
|
BotMsgProgressProcessingListPrefix Key = "bot.msg.progress.processing_list_prefix"
|
||||||
|
BotMsgProgressProcessingNone Key = "bot.msg.progress.processing_none"
|
||||||
|
BotMsgProgressSavePathPrefix Key = "bot.msg.progress.save_path_prefix"
|
||||||
|
BotMsgProgressTaskCanceled Key = "bot.msg.progress.task_canceled"
|
||||||
|
BotMsgProgressTaskCanceledWithId Key = "bot.msg.progress.task_canceled_with_id"
|
||||||
|
BotMsgProgressTaskFailedWithError Key = "bot.msg.progress.task_failed_with_error"
|
||||||
|
BotMsgProgressTelegraphDonePrefix Key = "bot.msg.progress.telegraph_done_prefix"
|
||||||
|
BotMsgProgressTelegraphProgressPrefix Key = "bot.msg.progress.telegraph_progress_prefix"
|
||||||
|
BotMsgProgressTelegraphStartPrefix Key = "bot.msg.progress.telegraph_start_prefix"
|
||||||
|
BotMsgProgressTotalSizePrefix Key = "bot.msg.progress.total_size_prefix"
|
||||||
|
BotMsgRuleErrorCreateRuleFailed Key = "bot.msg.rule.error_create_rule_failed"
|
||||||
|
BotMsgRuleErrorDeleteRuleFailed Key = "bot.msg.rule.error_delete_rule_failed"
|
||||||
|
BotMsgRuleErrorGetUserRulesFailed Key = "bot.msg.rule.error_get_user_rules_failed"
|
||||||
|
BotMsgRuleErrorInvalidRuleId Key = "bot.msg.rule.error_invalid_rule_id"
|
||||||
|
BotMsgRuleErrorInvalidRuleType Key = "bot.msg.rule.error_invalid_rule_type"
|
||||||
|
BotMsgRuleErrorUpdateUserFailed Key = "bot.msg.rule.error_update_user_failed"
|
||||||
|
BotMsgRuleHelpAddSuffix Key = "bot.msg.rule.help_add_suffix"
|
||||||
|
BotMsgRuleHelpAvailableOps Key = "bot.msg.rule.help_available_ops"
|
||||||
|
BotMsgRuleHelpCurrentModeDisabled Key = "bot.msg.rule.help_current_mode_disabled"
|
||||||
|
BotMsgRuleHelpCurrentModeEnabled Key = "bot.msg.rule.help_current_mode_enabled"
|
||||||
|
BotMsgRuleHelpDelSuffix Key = "bot.msg.rule.help_del_suffix"
|
||||||
|
BotMsgRuleHelpExistingRulesPrefix Key = "bot.msg.rule.help_existing_rules_prefix"
|
||||||
|
BotMsgRuleHelpSwitchSuffix Key = "bot.msg.rule.help_switch_suffix"
|
||||||
|
BotMsgRuleHelpUsage Key = "bot.msg.rule.help_usage"
|
||||||
|
BotMsgRuleInfoCreateRuleSuccess Key = "bot.msg.rule.info_create_rule_success"
|
||||||
|
BotMsgRuleInfoDeleteRuleSuccess Key = "bot.msg.rule.info_delete_rule_success"
|
||||||
|
BotMsgRuleInfoRuleModeDisabled Key = "bot.msg.rule.info_rule_mode_disabled"
|
||||||
|
BotMsgRuleInfoRuleModeEnabled Key = "bot.msg.rule.info_rule_mode_enabled"
|
||||||
|
BotMsgRulePromptProvideRuleId Key = "bot.msg.rule.prompt_provide_rule_id"
|
||||||
|
BotMsgSaveErrorInvalidIdOrUsername Key = "bot.msg.save.error_invalid_id_or_username"
|
||||||
BotMsgSaveHelpText Key = "bot.msg.save_help_text"
|
BotMsgSaveHelpText Key = "bot.msg.save_help_text"
|
||||||
|
BotMsgStorageInfoFilenamePrefix Key = "bot.msg.storage.info_filename_prefix"
|
||||||
|
BotMsgStorageInfoPromptSelectStorage Key = "bot.msg.storage.info_prompt_select_storage"
|
||||||
|
BotMsgTasksCancelFailed Key = "bot.msg.tasks.cancel_failed"
|
||||||
|
BotMsgTasksCancelRequestedPrefix Key = "bot.msg.tasks.cancel_requested_prefix"
|
||||||
|
BotMsgTasksFieldCreated Key = "bot.msg.tasks.field_created"
|
||||||
|
BotMsgTasksFieldId Key = "bot.msg.tasks.field_id"
|
||||||
|
BotMsgTasksFieldStatus Key = "bot.msg.tasks.field_status"
|
||||||
|
BotMsgTasksFieldTitle Key = "bot.msg.tasks.field_title"
|
||||||
|
BotMsgTasksInfoAddedToQueueFull Key = "bot.msg.tasks.info_added_to_queue_full"
|
||||||
|
BotMsgTasksInfoAddedToQueuePrefix Key = "bot.msg.tasks.info_added_to_queue_prefix"
|
||||||
|
BotMsgTasksInfoFilenamePrefix Key = "bot.msg.tasks.info_filename_prefix"
|
||||||
|
BotMsgTasksInfoQueueLengthPrefix Key = "bot.msg.tasks.info_queue_length_prefix"
|
||||||
|
BotMsgTasksQueuedEmpty Key = "bot.msg.tasks.queued_empty"
|
||||||
|
BotMsgTasksQueuedTitle Key = "bot.msg.tasks.queued_title"
|
||||||
|
BotMsgTasksRunningEmpty Key = "bot.msg.tasks.running_empty"
|
||||||
|
BotMsgTasksRunningTitle Key = "bot.msg.tasks.running_title"
|
||||||
|
BotMsgTasksStatusCancelRequested Key = "bot.msg.tasks.status_cancel_requested"
|
||||||
|
BotMsgTasksStatusQueued Key = "bot.msg.tasks.status_queued"
|
||||||
|
BotMsgTasksStatusRunning Key = "bot.msg.tasks.status_running"
|
||||||
|
BotMsgTasksTotalPrefix Key = "bot.msg.tasks.total_prefix"
|
||||||
|
BotMsgTasksTruncatedNote Key = "bot.msg.tasks.truncated_note"
|
||||||
|
BotMsgTasksUsage Key = "bot.msg.tasks.usage"
|
||||||
|
BotMsgTasksUsageCancel Key = "bot.msg.tasks.usage_cancel"
|
||||||
|
BotMsgTelegraphErrorBuildStorageSelectKeyboardFailed Key = "bot.msg.telegraph.error_build_storage_select_keyboard_failed"
|
||||||
|
BotMsgTelegraphInfoPicCountPrefix Key = "bot.msg.telegraph.info_pic_count_prefix"
|
||||||
|
BotMsgTelegraphInfoPromptSelectStorage Key = "bot.msg.telegraph.info_prompt_select_storage"
|
||||||
|
BotMsgTelegraphInfoTitlePrefix Key = "bot.msg.telegraph.info_title_prefix"
|
||||||
|
BotMsgUpdateButtonUpgrade Key = "bot.msg.update.button_upgrade"
|
||||||
|
BotMsgUpdateErrorCheckLatestFailed Key = "bot.msg.update.error_check_latest_failed"
|
||||||
|
BotMsgUpdateErrorNoReleaseFound Key = "bot.msg.update.error_no_release_found"
|
||||||
|
BotMsgUpdateErrorUpgradeFailed Key = "bot.msg.update.error_upgrade_failed"
|
||||||
|
BotMsgUpdateErrorVersionVarInvalid Key = "bot.msg.update.error_version_var_invalid"
|
||||||
|
BotMsgUpdateInfoAlreadyLatest Key = "bot.msg.update.info_already_latest"
|
||||||
|
BotMsgUpdateInfoMajorUpgradeRequired Key = "bot.msg.update.info_major_upgrade_required"
|
||||||
|
BotMsgUpdateInfoNewVersionInDocker Key = "bot.msg.update.info_new_version_in_docker"
|
||||||
|
BotMsgUpdateInfoNewVersionPromptUpgrade Key = "bot.msg.update.info_new_version_prompt_upgrade"
|
||||||
|
BotMsgUpdateInfoUpgradeSuccess Key = "bot.msg.update.info_upgrade_success"
|
||||||
|
BotMsgUpdateInfoUpgradingWithVersion Key = "bot.msg.update.info_upgrading_with_version"
|
||||||
|
BotMsgWatchErrorFilterFormatInvalid Key = "bot.msg.watch.error_filter_format_invalid"
|
||||||
|
BotMsgWatchErrorFilterTypeUnsupported Key = "bot.msg.watch.error_filter_type_unsupported"
|
||||||
|
BotMsgWatchErrorUnwatchChatFailed Key = "bot.msg.watch.error_unwatch_chat_failed"
|
||||||
|
BotMsgWatchErrorUnwatchNoChatProvided Key = "bot.msg.watch.error_unwatch_no_chat_provided"
|
||||||
|
BotMsgWatchErrorWatchChatFailed Key = "bot.msg.watch.error_watch_chat_failed"
|
||||||
|
BotMsgWatchInfoAlreadyWatchingChat Key = "bot.msg.watch.info_already_watching_chat"
|
||||||
|
BotMsgWatchInfoWatchChatStarted Key = "bot.msg.watch.info_watch_chat_started"
|
||||||
|
BotMsgWatchInfoWatchChatStopped Key = "bot.msg.watch.info_watch_chat_stopped"
|
||||||
|
BotMsgWatchInfoWatchListEmpty Key = "bot.msg.watch.info_watch_list_empty"
|
||||||
|
BotMsgWatchInfoWatchListFilterPrefix Key = "bot.msg.watch.info_watch_list_filter_prefix"
|
||||||
|
BotMsgWatchInfoWatchListHeader Key = "bot.msg.watch.info_watch_list_header"
|
||||||
BotMsgWatchHelpText Key = "bot.msg.watch_help_text"
|
BotMsgWatchHelpText Key = "bot.msg.watch_help_text"
|
||||||
ConfigErrDuplicateStorageName Key = "config.err.duplicate_storage_name"
|
ConfigErrDuplicateStorageName Key = "config.err.duplicate_storage_name"
|
||||||
ConfigErrInvalidCacheDir Key = "config.err.invalid_cache_dir"
|
ConfigErrInvalidCacheDir Key = "config.err.invalid_cache_dir"
|
||||||
ConfigLoadedStorages Key = "config.loaded_storages"
|
|
||||||
ErrCleanCacheFailed Key = "err.clean_cache_failed"
|
ErrCleanCacheFailed Key = "err.clean_cache_failed"
|
||||||
ErrGetCacheAbsPathFailed Key = "err.get_cache_abs_path_failed"
|
ErrGetCacheAbsPathFailed Key = "err.get_cache_abs_path_failed"
|
||||||
ErrGetWorkdirFailed Key = "err.get_workdir_failed"
|
ErrGetWorkdirFailed Key = "err.get_workdir_failed"
|
||||||
|
|||||||
324
common/i18n/locale/en.yaml
Normal file
324
common/i18n/locale/en.yaml
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
lifetime:
|
||||||
|
initing: Starting up
|
||||||
|
initfailed: Initialization failed
|
||||||
|
exiting: Shutting down
|
||||||
|
user_login_failed: "User login failed: {{.Error}}"
|
||||||
|
cleaning_cache: "Cleaning cache {{.Path}}"
|
||||||
|
bye: Exited
|
||||||
|
config:
|
||||||
|
err:
|
||||||
|
invalid_cache_dir: "Invalid cache directory: {{.Path}}, please check the config file"
|
||||||
|
duplicate_storage_name: "Storage name '{{.Name}}' is duplicated, please check the config file"
|
||||||
|
err:
|
||||||
|
get_workdir_failed: "Failed to get working directory: {{.Error}}"
|
||||||
|
get_cache_abs_path_failed: "Failed to get absolute cache path: {{.Error}}"
|
||||||
|
clean_cache_failed: "Failed to clean cache: {{.Error}}"
|
||||||
|
parser:
|
||||||
|
plugin:
|
||||||
|
load_failed: Failed to load parser plugins
|
||||||
|
loaded_dir: Parser plugins loaded
|
||||||
|
bot:
|
||||||
|
msg:
|
||||||
|
help_text_fmt: |
|
||||||
|
Save Any Bot - Save your Telegram files
|
||||||
|
Version: %s , Commit: %s
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
/start - Start using the bot
|
||||||
|
/help - Show help
|
||||||
|
/silent - Toggle silent mode
|
||||||
|
/storage - Set default storage
|
||||||
|
/save [custom filename] - Save file
|
||||||
|
/dir - Manage storage directories
|
||||||
|
/rule - Manage rules
|
||||||
|
/config - Modify configuration
|
||||||
|
/fnametmpl - Set custom filename template
|
||||||
|
/parser - Manage parser plugins
|
||||||
|
/task - Manage task queue
|
||||||
|
/watch - Watch chats and auto save (UserBot)
|
||||||
|
/unwatch - Stop watching chats (UserBot)
|
||||||
|
/lswatch - List watched chats (UserBot)
|
||||||
|
/update - Check and upgrade to latest version
|
||||||
|
|
||||||
|
Usage guide: https://sabot.unv.app/usage
|
||||||
|
cmd:
|
||||||
|
start: "Start using"
|
||||||
|
silent: "Toggle silent mode"
|
||||||
|
storage: "Set default storage"
|
||||||
|
dir: "Manage storage directories"
|
||||||
|
rule: "Manage auto-save rules"
|
||||||
|
save: "Save files"
|
||||||
|
dl: "Download files from given links"
|
||||||
|
task: "Manage task queue"
|
||||||
|
cancel: "Cancel task"
|
||||||
|
watch: "Watch chats (UserBot)"
|
||||||
|
unwatch: "Stop watching chats (UserBot)"
|
||||||
|
lswatch: "List watched chats (UserBot)"
|
||||||
|
config: "Modify configuration"
|
||||||
|
fnametmpl: "Set filename template"
|
||||||
|
help: "Show help"
|
||||||
|
parser: "Manage parsers"
|
||||||
|
update: "Check for updates"
|
||||||
|
save_help_text: |
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
1. Reply to the file you want to save with this command, optional filename parameter.
|
||||||
|
Example:
|
||||||
|
/save custom_file_name.mp4
|
||||||
|
|
||||||
|
2. After setting default storage, send /save <channel_id/username> <message_id_range> to batch save files. Rules will be applied; if no rule matches, default storage will be used.
|
||||||
|
Example:
|
||||||
|
/save @acherkrau 114-514
|
||||||
|
watch_help_text: |
|
||||||
|
Use /watch to watch messages in a chat and automatically save them to the default storage, following storage rules.
|
||||||
|
|
||||||
|
Syntax:
|
||||||
|
/watch <chat_id> [filter]
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- <chat_id>: Chat ID or username
|
||||||
|
- [filter]: Optional, format is filter_type:expression , see docs for all supported filters
|
||||||
|
|
||||||
|
Example:
|
||||||
|
/watch -1002229835658 msgre:.*plana.*
|
||||||
|
|
||||||
|
This will watch chat with ID -1002229835658 and save all media messages containing "plana".
|
||||||
|
common:
|
||||||
|
error_invalid_regex: "Invalid regex: {{.Error}}"
|
||||||
|
error_invalid_msg_id_range: "Invalid message ID range: {{.Error}}"
|
||||||
|
error_invalid_id_or_username: "Invalid ID or username: {{.Error}}"
|
||||||
|
error_get_messages_failed: "Failed to get messages: {{.Error}}"
|
||||||
|
info_fetching_messages: "Fetching messages..."
|
||||||
|
error_no_messages_in_range: "No messages found in the specified range"
|
||||||
|
error_no_savable_messages_in_range: "No savable messages found in the specified range"
|
||||||
|
error_build_storage_select_message_failed: "Failed to build storage selection message: {{.Error}}"
|
||||||
|
error_build_storage_select_keyboard_failed: "Failed to build storage selection keyboard: {{.Error}}"
|
||||||
|
info_found_files_select_storage: "Found {{.Count}} files, please select storage"
|
||||||
|
error_get_user_failed: "Failed to get user"
|
||||||
|
error_get_user_with_err_failed: "Failed to get user: {{.Error}}"
|
||||||
|
error_default_storage_not_set: "Please set a default storage first with /storage"
|
||||||
|
error_no_available_storage: "No available storage"
|
||||||
|
error_get_storage_failed: "Failed to get storage: {{.Error}}"
|
||||||
|
prompt_select_default_storage: "Please select a storage to set as default"
|
||||||
|
error_data_expired: "Data has expired or is invalid"
|
||||||
|
error_task_add_failed: "Failed to add task: {{.Error}}"
|
||||||
|
info_task_added: "Task added"
|
||||||
|
info_batch_tasks_added: "Batch tasks added, total {{.Count}} files"
|
||||||
|
error_task_create_failed: "Failed to create task: {{.Error}}"
|
||||||
|
error_get_dir_failed: "Failed to get directory: {{.Error}}"
|
||||||
|
prompt_select_dir: "Please select a directory to store to"
|
||||||
|
prompt_select_default_dir: "Please select a default directory to save to"
|
||||||
|
info_default_storage_set: "Default storage set to: {{.Name}}"
|
||||||
|
info_default_storage_with_dir_set: "Default storage set to: {{.Name}}:/{{.Dir}}"
|
||||||
|
error_get_user_info_failed: "Failed to get user info: {{.Error}}"
|
||||||
|
error_update_user_info_failed: "Failed to update user info: {{.Error}}"
|
||||||
|
info_silent_mode_on: "Silent mode enabled"
|
||||||
|
info_silent_mode_off: "Silent mode disabled"
|
||||||
|
error_get_file_failed: "Failed to get file: {{.Error}}"
|
||||||
|
info_fetching_file_info: "Fetching file info..."
|
||||||
|
error_no_savable_files_found: "No savable files found"
|
||||||
|
error_parse_telegraph_path_failed: "Failed to parse telegraph path: {{.Error}}"
|
||||||
|
info_fetching_telegraph_page: "Fetching telegraph page..."
|
||||||
|
error_get_telegraph_page_failed: "Failed to get telegraph page: {{.Error}}"
|
||||||
|
error_no_images_in_telegraph_page: "No images found in telegraph page"
|
||||||
|
error_build_dir_select_keyboard_failed: "Failed to build directory selection keyboard: {{.Error}}"
|
||||||
|
error_no_permission: |
|
||||||
|
You are not in the whitelist and cannot use this bot.
|
||||||
|
You can deploy your own instance: https://github.com/krau/SaveAny-Bot
|
||||||
|
save:
|
||||||
|
error_invalid_id_or_username: "Invalid ID or username: {{.Error}}"
|
||||||
|
watch:
|
||||||
|
error_filter_format_invalid: "Invalid filter format, please use <type>:<expression>"
|
||||||
|
error_filter_type_unsupported: "Unsupported filter type, please see the docs"
|
||||||
|
error_watch_chat_failed: "Failed to watch chat: {{.Error}}"
|
||||||
|
info_watch_chat_started: "Started watching chat: {{.Chat}}"
|
||||||
|
info_already_watching_chat: "Already watching this chat"
|
||||||
|
info_watch_list_empty: "No chats are being watched currently"
|
||||||
|
info_watch_list_header: "Currently watched chats:\n"
|
||||||
|
info_watch_list_filter_prefix: " (filter: "
|
||||||
|
error_unwatch_no_chat_provided: "Please provide a chat ID or username to unwatch"
|
||||||
|
error_unwatch_chat_failed: "Failed to unwatch chat: {{.Error}}"
|
||||||
|
info_watch_chat_stopped: "Stopped watching chat: {{.Chat}}"
|
||||||
|
tasks:
|
||||||
|
usage_cancel: "Usage: /tasks cancel <task_id>"
|
||||||
|
usage: "Usage: /tasks [running|queued|cancel <task_id>]"
|
||||||
|
cancel_failed: "Failed to cancel task: {{.Error}}"
|
||||||
|
cancel_requested_prefix: "Cancel requested for task: "
|
||||||
|
running_empty: "No running tasks"
|
||||||
|
running_title: "Currently running tasks:"
|
||||||
|
total_prefix: "Total: {{.Count}}\n"
|
||||||
|
field_id: "ID: "
|
||||||
|
field_title: "Title: "
|
||||||
|
field_created: "Created at: "
|
||||||
|
field_status: "Status: "
|
||||||
|
status_running: "Running"
|
||||||
|
status_queued: "Queued"
|
||||||
|
status_cancel_requested: "Cancel requested"
|
||||||
|
queued_empty: "No queued tasks"
|
||||||
|
queued_title: "Currently queued tasks:"
|
||||||
|
truncated_note: "...\nShowing first 10 tasks, total {{.Count}} tasks"
|
||||||
|
info_added_to_queue_full: "Added to task queue\nFilename: {{.Filename}}\nCurrent queued tasks: {{.QueueLength}}"
|
||||||
|
info_added_to_queue_prefix: "Added to task queue\n"
|
||||||
|
info_filename_prefix: "Filename: "
|
||||||
|
info_queue_length_prefix: "\nCurrent queued tasks: "
|
||||||
|
rule:
|
||||||
|
error_get_user_rules_failed: "Failed to get user rules"
|
||||||
|
error_update_user_failed: "Failed to update user"
|
||||||
|
info_rule_mode_enabled: "Rule mode enabled"
|
||||||
|
info_rule_mode_disabled: "Rule mode disabled"
|
||||||
|
error_invalid_rule_type: "Invalid rule type: {{.Type}}\nAvailable: {{.Available}}"
|
||||||
|
error_create_rule_failed: "Failed to create rule"
|
||||||
|
info_create_rule_success: "Rule created successfully"
|
||||||
|
prompt_provide_rule_id: "Please provide rule ID"
|
||||||
|
error_invalid_rule_id: "Invalid rule ID"
|
||||||
|
error_delete_rule_failed: "Failed to delete rule"
|
||||||
|
info_delete_rule_success: "Rule deleted successfully"
|
||||||
|
help_usage: "Usage: /rule <op> <args...>"
|
||||||
|
help_current_mode_enabled: "\nRule mode is currently enabled"
|
||||||
|
help_current_mode_disabled: "\nRule mode is currently disabled"
|
||||||
|
help_available_ops: "\n\nAvailable operations:\n"
|
||||||
|
help_switch_suffix: " - Toggle rule mode\n"
|
||||||
|
help_add_suffix: " <type> <data> <storage_name> <path> - Add rule\n"
|
||||||
|
help_del_suffix: " <rule_id> - Delete rule\n"
|
||||||
|
help_existing_rules_prefix: "\nCurrent rules:\n"
|
||||||
|
dir:
|
||||||
|
error_get_user_dirs_failed: "Failed to get user directories"
|
||||||
|
error_get_user_failed: "Failed to get user"
|
||||||
|
error_create_dir_failed: "Failed to create directory"
|
||||||
|
info_create_dir_success: "Directory created successfully"
|
||||||
|
error_invalid_dir_id: "Invalid directory ID"
|
||||||
|
error_delete_dir_failed: "Failed to delete directory"
|
||||||
|
info_delete_dir_success: "Directory deleted successfully"
|
||||||
|
error_unknown_operation: "Unknown operation"
|
||||||
|
help_usage: "Usage: /dir <op> <args...>"
|
||||||
|
help_available_ops: "\n\nAvailable operations:\n"
|
||||||
|
help_add_suffix: " <storage_name> <path> - Add path\n"
|
||||||
|
help_del_suffix: " <path_id> - Delete path\n"
|
||||||
|
help_add_example_prefix: "\nAdd path example:\n"
|
||||||
|
help_add_example_cmd: "/dir add local1 path/to/dir"
|
||||||
|
help_del_example_prefix: "\n\nDelete path example:\n"
|
||||||
|
help_del_example_cmd: "/dir del 3"
|
||||||
|
help_existing_dirs_prefix: "\n\nCurrent paths:\n"
|
||||||
|
button_default: "Default"
|
||||||
|
parser:
|
||||||
|
help_text: |
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
/parser install <reply a file> - Install parser
|
||||||
|
plugin_not_enabled: "Parser plugin feature is not enabled"
|
||||||
|
prompt_reply_with_parser_file: "Please reply with a message containing the parser file"
|
||||||
|
error_no_valid_file_in_reply: "The replied message does not contain a valid file"
|
||||||
|
error_wrong_file_type: "Wrong file type"
|
||||||
|
error_file_too_large: "File too large"
|
||||||
|
error_get_filename_failed: "Failed to get filename"
|
||||||
|
error_only_js_supported: "Only .js files are supported as parsers"
|
||||||
|
error_download_file_failed: "Failed to download file: {{.Error}}"
|
||||||
|
error_install_plugin_failed: "Failed to install plugin: {{.Error}}"
|
||||||
|
info_install_plugin_success: "Plugin installed: {{.Name}}"
|
||||||
|
parse:
|
||||||
|
info_parsing: "Parsing..."
|
||||||
|
error_parse_text_failed: "Failed to parse text: {{.Error}}"
|
||||||
|
error_build_storage_select_keyboard_failed: "Failed to build storage selection keyboard: {{.Error}}"
|
||||||
|
error_build_parsed_text_entity_failed: "Failed to build parsed text entity: {{.Error}}"
|
||||||
|
info_link_prefix: "\nLink: "
|
||||||
|
info_author_prefix: "\nAuthor: "
|
||||||
|
info_description_prefix: "\nDescription: "
|
||||||
|
info_file_count_prefix: "\nFile count: "
|
||||||
|
info_total_size_prefix: "\nEstimated total size: "
|
||||||
|
info_prompt_select_storage: "\nPlease select storage"
|
||||||
|
telegraph:
|
||||||
|
error_build_storage_select_keyboard_failed: "Failed to build storage selection keyboard: {{.Error}}"
|
||||||
|
info_title_prefix: "Title: "
|
||||||
|
info_pic_count_prefix: "\nImage count: "
|
||||||
|
info_prompt_select_storage: "\nPlease select storage"
|
||||||
|
update:
|
||||||
|
error_version_var_invalid: "Currently in development version or version info injection failed: {{.Error}}"
|
||||||
|
error_check_latest_failed: "Failed to check latest version: {{.Error}}"
|
||||||
|
error_no_release_found: "No release found"
|
||||||
|
info_major_upgrade_required: "Major upgrade detected: {{.Current}} -> {{.Latest}} , please download the latest version from GitHub and check the migration guide"
|
||||||
|
info_already_latest: "Already on latest version: {{.Version}}"
|
||||||
|
info_new_version_in_docker: |-
|
||||||
|
New version found: {{.Latest}}
|
||||||
|
Current version: {{.Current}}
|
||||||
|
Published at: {{.PublishedAt}}
|
||||||
|
Since you are using Docker, please update it on your deployment platform
|
||||||
|
info_new_version_prompt_upgrade: |-
|
||||||
|
New version found: {{.Latest}}
|
||||||
|
Current version: {{.Current}}
|
||||||
|
|
||||||
|
File size: {{.SizeMB}} MB
|
||||||
|
Download URL: {{.URL}}
|
||||||
|
Published at: {{.PublishedAt}}
|
||||||
|
|
||||||
|
Upgrading will restart the bot. Proceed?
|
||||||
|
info_upgrading_with_version: "Upgrading, current version: {{.Current}}"
|
||||||
|
error_upgrade_failed: "Upgrade failed: {{.Error}}"
|
||||||
|
info_upgrade_success: "Upgraded to version {{.Version}}\nIf the bot did not restart automatically, please start it manually"
|
||||||
|
button_upgrade: "Upgrade"
|
||||||
|
config:
|
||||||
|
prompt_select_option: "Please select an option to configure"
|
||||||
|
button_filename_strategy: "Filename strategy"
|
||||||
|
error_invalid_callback_data: "Invalid callback data"
|
||||||
|
error_invalid_template: "Invalid template, please check syntax\n{{.Error}}"
|
||||||
|
info_filename_strategy_set: "Filename strategy set to: {{.Strategy}}"
|
||||||
|
prompt_select_filename_strategy: "Please select filename strategy, current strategy: {{.Strategy}}"
|
||||||
|
fnametmpl_help: |-
|
||||||
|
Use this command to set filename template, for example:
|
||||||
|
/fnametmpl Image_{{"{{.msgid}}"}}_{{"{{.msgdate}}"}}.jpg
|
||||||
|
|
||||||
|
Available variables:
|
||||||
|
- {{"{{.msgid}}"}}: Message ID
|
||||||
|
- {{"{{.msgtags}}"}}: Tags in the message, joined with underscore
|
||||||
|
- {{"{{.msggen}}"}}: Generated filename from the message
|
||||||
|
- {{"{{.msgdate}}"}}: Message date, format YYYY-MM-DD_HH-MM-SS
|
||||||
|
- {{"{{.origname}}"}}: Original media filename (if any)
|
||||||
|
- {{"{{.chatid}}"}}: Chat ID of the message
|
||||||
|
|
||||||
|
Template only takes effect when filename strategy is set to 'Custom template'.
|
||||||
|
If template parsing fails, it will fall back to default filename.
|
||||||
|
info_template_updated: "Filename template updated"
|
||||||
|
info_current_template_prefix: "Current template: {{.Template}}"
|
||||||
|
dl:
|
||||||
|
usage: "Usage: /dl <url1> <url2> ..."
|
||||||
|
error_no_valid_links: "No valid links to download"
|
||||||
|
info_files_select_storage: "Total {{.Count}} files, please select storage"
|
||||||
|
cancel:
|
||||||
|
usage: "Usage: /cancel <task_id>"
|
||||||
|
error_cancel_failed: "Failed to cancel task: {{.Error}}"
|
||||||
|
info_cancel_requested: "Cancel requested for task: {{.TaskID}}"
|
||||||
|
info_cancelling_task: "Cancelling task..."
|
||||||
|
media_group:
|
||||||
|
info_saving_files: "Saving files..."
|
||||||
|
error_build_storage_select_keyboard_failed: "Failed to build storage selection keyboard: {{.Error}}"
|
||||||
|
info_group_found_files_select_storage: "Total {{.Count}} files, please select storage"
|
||||||
|
storage:
|
||||||
|
info_filename_prefix: "Filename: "
|
||||||
|
info_prompt_select_storage: "\nPlease select storage"
|
||||||
|
progress:
|
||||||
|
batch_start_prefix: "Starting batch download task\nTotal size: "
|
||||||
|
batch_processing_prefix: "Processing batch download task\nTotal size: "
|
||||||
|
downloading_prefix: "Downloading\nTotal size: "
|
||||||
|
processing_list_prefix: "\nProcessing:\n"
|
||||||
|
processing_none: " - None"
|
||||||
|
avg_speed_prefix: "\nAverage speed: "
|
||||||
|
current_progress_prefix: "\nCurrent progress: "
|
||||||
|
task_canceled: "Task canceled"
|
||||||
|
task_canceled_with_id: "Processing canceled: {{.TaskID}}"
|
||||||
|
task_failed_with_error: "Processing failed: {{.Error}}"
|
||||||
|
batch_done_prefix: "Completed\nFile count: "
|
||||||
|
direct_done_prefix: "Completed, file count: "
|
||||||
|
parsed_start_prefix: "Starting download from {{.Site}}\nTotal size: "
|
||||||
|
parsed_done_prefix: "Completed, resource count: "
|
||||||
|
telegraph_start_prefix: "Starting Telegraph download\nImage count: "
|
||||||
|
telegraph_progress_prefix: "Downloading\nCurrent progress: "
|
||||||
|
telegraph_done_prefix: "Completed\nImage count: "
|
||||||
|
file_start_prefix: "Starting download\nFilename: "
|
||||||
|
file_processing_prefix: "Processing download task\nFilename: "
|
||||||
|
download_failed_prefix: "Download failed\nFilename: "
|
||||||
|
download_done_prefix: "Download completed\nFilename: "
|
||||||
|
file_size_prefix: "\nFile size: "
|
||||||
|
save_path_prefix: "\nSave path: "
|
||||||
|
total_size_prefix: "\nTotal size: "
|
||||||
|
direct_start: "Starting download, total size: {{.SizeMB}} MB ({{.Count}} files)"
|
||||||
|
file_name_prefix: "Filename: "
|
||||||
|
error_prefix: "\nError: "
|
||||||
@@ -6,7 +6,6 @@ lifetime:
|
|||||||
cleaning_cache: "正在清理缓存 {{.Path}}"
|
cleaning_cache: "正在清理缓存 {{.Path}}"
|
||||||
bye: 已退出
|
bye: 已退出
|
||||||
config:
|
config:
|
||||||
loaded_storages: "已加载 {{.Count}} 个存储后端"
|
|
||||||
err:
|
err:
|
||||||
invalid_cache_dir: "无效的缓存目录: {{.Path}},请检查配置文件"
|
invalid_cache_dir: "无效的缓存目录: {{.Path}},请检查配置文件"
|
||||||
duplicate_storage_name: "存储名称 '{{.Name}}' 重复,请检查配置文件"
|
duplicate_storage_name: "存储名称 '{{.Name}}' 重复,请检查配置文件"
|
||||||
@@ -35,11 +34,31 @@ bot:
|
|||||||
/config - 修改配置
|
/config - 修改配置
|
||||||
/fnametmpl - 设置文件自定义命名模板
|
/fnametmpl - 设置文件自定义命名模板
|
||||||
/parser - 管理解析器插件
|
/parser - 管理解析器插件
|
||||||
|
/task - 管理任务队列
|
||||||
/watch - 监听聊天并自动保存 (UserBot)
|
/watch - 监听聊天并自动保存 (UserBot)
|
||||||
|
/unwatch - 取消监听聊天 (UserBot)
|
||||||
|
/lswatch - 列出正在监听的聊天 (UserBot)
|
||||||
/update - 检查更新并升级
|
/update - 检查更新并升级
|
||||||
|
|
||||||
使用帮助: https://sabot.unv.app/usage
|
使用帮助: https://sabot.unv.app/usage
|
||||||
反馈群组: https://t.me/ProjectSaveAny
|
cmd:
|
||||||
|
start: "开始使用"
|
||||||
|
silent: "切换静默模式"
|
||||||
|
storage: "设置默认存储端"
|
||||||
|
dir: "管理存储文件夹"
|
||||||
|
rule: "管理自动存储规则"
|
||||||
|
save: "保存文件"
|
||||||
|
dl: "下载给定链接的文件"
|
||||||
|
task: "管理任务队列"
|
||||||
|
cancel: "取消任务"
|
||||||
|
watch: "监听聊天(UserBot)"
|
||||||
|
unwatch: "取消监听聊天(UserBot)"
|
||||||
|
lswatch: "列出监听的聊天(UserBot)"
|
||||||
|
config: "修改配置"
|
||||||
|
fnametmpl: "设置文件命名模板"
|
||||||
|
help: "显示帮助"
|
||||||
|
parser: "管理解析器"
|
||||||
|
update: "检查更新"
|
||||||
save_help_text: |
|
save_help_text: |
|
||||||
使用方法:
|
使用方法:
|
||||||
|
|
||||||
@@ -64,3 +83,242 @@ bot:
|
|||||||
/watch -1002229835658 msgre:.*plana.*
|
/watch -1002229835658 msgre:.*plana.*
|
||||||
|
|
||||||
这将监听 ID 为 -1002229835658 的聊天, 并转存所有包含 "plana" 的媒体消息
|
这将监听 ID 为 -1002229835658 的聊天, 并转存所有包含 "plana" 的媒体消息
|
||||||
|
common:
|
||||||
|
error_invalid_regex: "无效的正则表达式: {{.Error}}"
|
||||||
|
error_invalid_msg_id_range: "无效的消息ID范围: {{.Error}}"
|
||||||
|
error_invalid_id_or_username: "无效的ID或用户名: {{.Error}}"
|
||||||
|
error_get_messages_failed: "获取消息失败: {{.Error}}"
|
||||||
|
info_fetching_messages: "正在获取消息..."
|
||||||
|
error_no_messages_in_range: "没有找到指定范围内的消息"
|
||||||
|
error_no_savable_messages_in_range: "没有找到指定范围内的可保存消息"
|
||||||
|
error_build_storage_select_message_failed: "构建存储选择消息失败: {{.Error}}"
|
||||||
|
error_build_storage_select_keyboard_failed: "构建存储选择键盘失败: {{.Error}}"
|
||||||
|
info_found_files_select_storage: "找到 {{.Count}} 个文件, 请选择存储位置"
|
||||||
|
error_get_user_failed: "获取用户失败"
|
||||||
|
error_get_user_with_err_failed: "获取用户失败: {{.Error}}"
|
||||||
|
error_default_storage_not_set: "请先设置默认存储, 使用 /storage 命令"
|
||||||
|
error_no_available_storage: "无可用的存储"
|
||||||
|
error_get_storage_failed: "获取存储失败: {{.Error}}"
|
||||||
|
prompt_select_default_storage: "请选择要设为默认的存储位置"
|
||||||
|
error_data_expired: "数据已过期或无效"
|
||||||
|
error_task_add_failed: "任务添加失败: {{.Error}}"
|
||||||
|
info_task_added: "任务已添加"
|
||||||
|
info_batch_tasks_added: "已添加批量任务, 共 {{.Count}} 个文件"
|
||||||
|
error_task_create_failed: "任务创建失败: {{.Error}}"
|
||||||
|
error_get_dir_failed: "获取目录失败: {{.Error}}"
|
||||||
|
prompt_select_dir: "请选择要存储到的目录"
|
||||||
|
prompt_select_default_dir: "请选择要保存到的默认文件夹"
|
||||||
|
info_default_storage_set: "已将默认存储位置设为: {{.Name}}"
|
||||||
|
info_default_storage_with_dir_set: "已将默认存储位置设为: {{.Name}}:/{{.Dir}}"
|
||||||
|
error_get_user_info_failed: "获取用户信息失败: {{.Error}}"
|
||||||
|
error_update_user_info_failed: "更新用户信息失败: {{.Error}}"
|
||||||
|
info_silent_mode_on: "已开启静默模式"
|
||||||
|
info_silent_mode_off: "已关闭静默模式"
|
||||||
|
error_get_file_failed: "获取文件失败: {{.Error}}"
|
||||||
|
info_fetching_file_info: "正在获取文件信息..."
|
||||||
|
error_no_savable_files_found: "没有找到可保存的文件"
|
||||||
|
error_parse_telegraph_path_failed: "解析 telegraph 路径失败: {{.Error}}"
|
||||||
|
info_fetching_telegraph_page: "正在获取 telegraph 页面..."
|
||||||
|
error_get_telegraph_page_failed: "获取 telegraph 页面失败: {{.Error}}"
|
||||||
|
error_no_images_in_telegraph_page: "在 telegraph 页面中未找到图片"
|
||||||
|
error_build_dir_select_keyboard_failed: "构建目录选择键盘失败: {{.Error}}"
|
||||||
|
error_no_permission: |
|
||||||
|
您不在白名单中, 无法使用此 Bot.
|
||||||
|
您可以部署自己的实例: https://github.com/krau/SaveAny-Bot
|
||||||
|
save:
|
||||||
|
error_invalid_id_or_username: "无效的ID或用户名: {{.Error}}"
|
||||||
|
watch:
|
||||||
|
error_filter_format_invalid: "过滤器格式错误, 请使用 <过滤器类型>:<表达式>"
|
||||||
|
error_filter_type_unsupported: "不支持的过滤器类型, 请参阅文档"
|
||||||
|
error_watch_chat_failed: "监听聊天失败: {{.Error}}"
|
||||||
|
info_watch_chat_started: "已开始监听聊天: {{.Chat}}"
|
||||||
|
info_already_watching_chat: "已经在监听此聊天"
|
||||||
|
info_watch_list_empty: "当前没有监听任何聊天"
|
||||||
|
info_watch_list_header: "当前监听的聊天:\n"
|
||||||
|
info_watch_list_filter_prefix: " (过滤器: "
|
||||||
|
error_unwatch_no_chat_provided: "请提供要取消监听的聊天ID或用户名"
|
||||||
|
error_unwatch_chat_failed: "取消监听聊天失败: {{.Error}}"
|
||||||
|
info_watch_chat_stopped: "已取消监听聊天: {{.Chat}}"
|
||||||
|
tasks:
|
||||||
|
usage_cancel: "用法: /tasks cancel <task_id>"
|
||||||
|
usage: "用法: /tasks [running|queued|cancel <task_id>]"
|
||||||
|
cancel_failed: "取消任务失败: {{.Error}}"
|
||||||
|
cancel_requested_prefix: "已请求取消任务: "
|
||||||
|
running_empty: "当前没有正在运行的任务"
|
||||||
|
running_title: "当前正在运行的任务:"
|
||||||
|
total_prefix: "总数: {{.Count}}\n"
|
||||||
|
field_id: "ID: "
|
||||||
|
field_title: "名称: "
|
||||||
|
field_created: "创建时间: "
|
||||||
|
field_status: "状态: "
|
||||||
|
status_running: "运行中"
|
||||||
|
status_queued: "排队中"
|
||||||
|
status_cancel_requested: "已请求取消"
|
||||||
|
queued_empty: "当前没有排队中的任务"
|
||||||
|
queued_title: "当前排队中的任务:"
|
||||||
|
truncated_note: "...\n只显示前 10 个任务, 共 {{.Count}} 个任务"
|
||||||
|
info_added_to_queue_full: "已添加到任务队列\n文件名: {{.Filename}}\n当前排队任务数: {{.QueueLength}}"
|
||||||
|
info_added_to_queue_prefix: "已添加到任务队列\n"
|
||||||
|
info_filename_prefix: "文件名: "
|
||||||
|
info_queue_length_prefix: "\n当前排队任务数: "
|
||||||
|
rule:
|
||||||
|
error_get_user_rules_failed: "获取用户规则失败"
|
||||||
|
error_update_user_failed: "更新用户失败"
|
||||||
|
info_rule_mode_enabled: "已启用规则模式"
|
||||||
|
info_rule_mode_disabled: "已禁用规则模式"
|
||||||
|
error_invalid_rule_type: "无效的规则类型: {{.Type}}\n可用: {{.Available}}"
|
||||||
|
error_create_rule_failed: "创建规则失败"
|
||||||
|
info_create_rule_success: "创建规则成功"
|
||||||
|
prompt_provide_rule_id: "请提供规则ID"
|
||||||
|
error_invalid_rule_id: "无效的规则ID"
|
||||||
|
error_delete_rule_failed: "删除规则失败"
|
||||||
|
info_delete_rule_success: "删除规则成功"
|
||||||
|
help_usage: "使用方法: /rule <操作> <参数...>"
|
||||||
|
help_current_mode_enabled: "\n当前已启用规则模式"
|
||||||
|
help_current_mode_disabled: "\n当前已禁用规则模式"
|
||||||
|
help_available_ops: "\n\n可用操作:\n"
|
||||||
|
help_switch_suffix: " - 开关规则模式\n"
|
||||||
|
help_add_suffix: " <类型> <数据> <存储名> <路径> - 添加规则\n"
|
||||||
|
help_del_suffix: " <规则ID> - 删除规则\n"
|
||||||
|
help_existing_rules_prefix: "\n当前已添加的规则:\n"
|
||||||
|
dir:
|
||||||
|
error_get_user_dirs_failed: "获取用户文件夹失败"
|
||||||
|
error_get_user_failed: "获取用户失败"
|
||||||
|
error_create_dir_failed: "创建文件夹失败"
|
||||||
|
info_create_dir_success: "文件夹添加成功"
|
||||||
|
error_invalid_dir_id: "文件夹ID无效"
|
||||||
|
error_delete_dir_failed: "删除文件夹失败"
|
||||||
|
info_delete_dir_success: "文件夹删除成功"
|
||||||
|
error_unknown_operation: "未知操作"
|
||||||
|
help_usage: "使用方法: /dir <操作> <参数...>"
|
||||||
|
help_available_ops: "\n\n可用操作:\n"
|
||||||
|
help_add_suffix: " <存储名> <路径> - 添加路径\n"
|
||||||
|
help_del_suffix: " <路径ID> - 删除路径\n"
|
||||||
|
help_add_example_prefix: "\n添加路径示例:\n"
|
||||||
|
help_add_example_cmd: "/dir add local1 path/to/dir"
|
||||||
|
help_del_example_prefix: "\n\n删除路径示例:\n"
|
||||||
|
help_del_example_cmd: "/dir del 3"
|
||||||
|
help_existing_dirs_prefix: "\n\n当前已添加的路径:\n"
|
||||||
|
button_default: "默认"
|
||||||
|
parser:
|
||||||
|
help_text: |
|
||||||
|
用法:
|
||||||
|
|
||||||
|
/parser install <回复一个文件> - 安装解析器
|
||||||
|
plugin_not_enabled: "解析器插件功能未启用"
|
||||||
|
prompt_reply_with_parser_file: "请回复一个包含解析器文件的消息"
|
||||||
|
error_no_valid_file_in_reply: "回复的消息不包含有效的文件"
|
||||||
|
error_wrong_file_type: "错误的文件类型"
|
||||||
|
error_file_too_large: "文件过大"
|
||||||
|
error_get_filename_failed: "无法获取文件名"
|
||||||
|
error_only_js_supported: "仅支持 .js 文件作为解析器"
|
||||||
|
error_download_file_failed: "文件下载失败: {{.Error}}"
|
||||||
|
error_install_plugin_failed: "插件安装失败: {{.Error}}"
|
||||||
|
info_install_plugin_success: "插件安装成功: {{.Name}}"
|
||||||
|
parse:
|
||||||
|
info_parsing: "正在解析..."
|
||||||
|
error_parse_text_failed: "Failed to parse text: {{.Error}}"
|
||||||
|
error_build_storage_select_keyboard_failed: "Failed to build storage selection keyboard: {{.Error}}"
|
||||||
|
error_build_parsed_text_entity_failed: "Failed to build parsed text entity: {{.Error}}"
|
||||||
|
info_link_prefix: "\n链接: "
|
||||||
|
info_author_prefix: "\n作者: "
|
||||||
|
info_description_prefix: "\n描述: "
|
||||||
|
info_file_count_prefix: "\n文件数量: "
|
||||||
|
info_total_size_prefix: "\n预计总大小: "
|
||||||
|
info_prompt_select_storage: "\n请选择存储位置"
|
||||||
|
telegraph:
|
||||||
|
error_build_storage_select_keyboard_failed: "构建存储选择键盘失败: {{.Error}}"
|
||||||
|
info_title_prefix: "标题: "
|
||||||
|
info_pic_count_prefix: "\n图片数量: "
|
||||||
|
info_prompt_select_storage: "\n请选择存储位置"
|
||||||
|
update:
|
||||||
|
error_version_var_invalid: "当前处于开发版本或版本信息注入失败: {{.Error}}"
|
||||||
|
error_check_latest_failed: "检测最新版本失败: {{.Error}}"
|
||||||
|
error_no_release_found: "没有找到版本信息"
|
||||||
|
info_major_upgrade_required: "检测到大版本更新: {{.Current}} -> {{.Latest}} , 请前往 GitHub 手动下载最新版本并查看迁移指南"
|
||||||
|
info_already_latest: "当前已经是最新版本: {{.Version}}"
|
||||||
|
info_new_version_in_docker: |-
|
||||||
|
发现新版本: {{.Latest}}
|
||||||
|
当前版本: {{.Current}}
|
||||||
|
发布时间: {{.PublishedAt}}
|
||||||
|
由于您正在使用 Docker 部署, 请自行在部署平台上执行更新命令
|
||||||
|
info_new_version_prompt_upgrade: |-
|
||||||
|
发现新版本: {{.Latest}}
|
||||||
|
当前版本: {{.Current}}
|
||||||
|
|
||||||
|
文件大小: {{.SizeMB}} MB
|
||||||
|
下载链接: {{.URL}}
|
||||||
|
发布时间: {{.PublishedAt}}
|
||||||
|
|
||||||
|
升级将重启 Bot , 是否升级?
|
||||||
|
info_upgrading_with_version: "正在升级中, 当前版本: {{.Current}}"
|
||||||
|
error_upgrade_failed: "升级失败: {{.Error}}"
|
||||||
|
info_upgrade_success: "已升级至版本 {{.Version}}\n若 Bot 未自动重启请手动启动"
|
||||||
|
button_upgrade: "升级"
|
||||||
|
config:
|
||||||
|
prompt_select_option: "请选择要配置的选项"
|
||||||
|
button_filename_strategy: "文件名策略"
|
||||||
|
error_invalid_callback_data: "无效的回调数据"
|
||||||
|
error_invalid_template: "无效的模板, 请检查语法\n{{.Error}}"
|
||||||
|
info_filename_strategy_set: "已将文件名策略设置为: {{.Strategy}}"
|
||||||
|
prompt_select_filename_strategy: "请选择文件名策略, 当前策略: {{.Strategy}}"
|
||||||
|
fnametmpl_help: |-
|
||||||
|
使用该命令设置文件名模板, 示例:
|
||||||
|
/fnametmpl 图片_{{"{{.msgid}}"}}_{{"{{.msgdate}}"}}.jpg
|
||||||
|
|
||||||
|
可用变量:
|
||||||
|
- {{"{{.msgid}}"}}: 消息ID
|
||||||
|
- {{"{{.msgtags}}"}}: 消息中的标签, 将以下划线分隔输出
|
||||||
|
- {{"{{.msggen}}"}}: 根据消息生成的文件名
|
||||||
|
- {{"{{.msgdate}}"}}: 消息日期, 格式 YYYY-MM-DD_HH-MM-SS
|
||||||
|
- {{"{{.origname}}"}}: 媒体的原始文件名 (如果有)
|
||||||
|
- {{"{{.chatid}}"}}: 消息的聊天ID
|
||||||
|
|
||||||
|
模板仅在文件名策略设置为 '自定义模板' 时生效,
|
||||||
|
且模板解析错误时会回退到默认文件名
|
||||||
|
info_template_updated: "已更新文件名模板"
|
||||||
|
info_current_template_prefix: "当前模板: {{.Template}}"
|
||||||
|
dl:
|
||||||
|
usage: "用法: /dl <链接1> <链接2> ..."
|
||||||
|
error_no_valid_links: "没有有效的链接可供下载"
|
||||||
|
info_files_select_storage: "共 {{.Count}} 个文件, 请选择存储位置"
|
||||||
|
cancel:
|
||||||
|
usage: "用法: /cancel <task_id>"
|
||||||
|
error_cancel_failed: "取消任务失败: {{.Error}}"
|
||||||
|
info_cancel_requested: "已请求取消任务: {{.TaskID}}"
|
||||||
|
info_cancelling_task: "正在取消任务..."
|
||||||
|
media_group:
|
||||||
|
info_saving_files: "正在保存文件..."
|
||||||
|
error_build_storage_select_keyboard_failed: "构建存储选择键盘失败: {{.Error}}"
|
||||||
|
info_group_found_files_select_storage: "共 {{.Count}} 个文件, 请选择存储位置"
|
||||||
|
storage:
|
||||||
|
info_filename_prefix: "文件名: "
|
||||||
|
info_prompt_select_storage: "\n请选择存储位置"
|
||||||
|
progress:
|
||||||
|
batch_start_prefix: "开始执行批量下载任务\n总大小: "
|
||||||
|
batch_processing_prefix: "正在处理批量下载任务\n总大小: "
|
||||||
|
downloading_prefix: "正在下载\n总大小: "
|
||||||
|
processing_list_prefix: "\n正在处理:\n"
|
||||||
|
processing_none: " - 无"
|
||||||
|
avg_speed_prefix: "\n平均速度: "
|
||||||
|
current_progress_prefix: "\n当前进度: "
|
||||||
|
task_canceled: "任务已取消"
|
||||||
|
task_canceled_with_id: "处理已取消: {{.TaskID}}"
|
||||||
|
task_failed_with_error: "处理失败: {{.Error}}"
|
||||||
|
batch_done_prefix: "处理完成\n文件数: "
|
||||||
|
direct_done_prefix: "处理完成, 文件数量: "
|
||||||
|
parsed_start_prefix: "开始下载 {{.Site}} 的资源\n总大小: "
|
||||||
|
parsed_done_prefix: "处理完成, 资源数量: "
|
||||||
|
telegraph_start_prefix: "开始下载Telegraph\n图片数量: "
|
||||||
|
telegraph_progress_prefix: "正在下载\n当前进度: "
|
||||||
|
telegraph_done_prefix: "处理完成\n图片数量: "
|
||||||
|
file_start_prefix: "开始下载\n文件名: "
|
||||||
|
file_processing_prefix: "正在处理下载任务\n文件名: "
|
||||||
|
download_failed_prefix: "下载失败\n文件名: "
|
||||||
|
download_done_prefix: "下载完成\n文件名: "
|
||||||
|
file_size_prefix: "\n文件大小: "
|
||||||
|
save_path_prefix: "\n保存路径: "
|
||||||
|
total_size_prefix: "\n总大小: "
|
||||||
|
direct_start: "开始下载, 总大小: {{.SizeMB}} MB ({{.Count}} 个文件)"
|
||||||
|
file_name_prefix: "文件名: "
|
||||||
|
error_prefix: "\n错误: "
|
||||||
|
|||||||
65
common/utils/ioutil/progress_reader.go
Normal file
65
common/utils/ioutil/progress_reader.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package ioutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ io.ReadSeeker = (*ProgressReadSeeker)(nil)
|
||||||
|
|
||||||
|
// ProgressReadSeeker wraps an io.ReadSeeker and tracks read progress
|
||||||
|
type ProgressReadSeeker struct {
|
||||||
|
reader io.ReadSeeker
|
||||||
|
total atomic.Int64
|
||||||
|
read atomic.Int64
|
||||||
|
onProgress func(read int64, total int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek implements io.ReadSeeker.
|
||||||
|
func (pr *ProgressReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
return pr.reader.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProgressReader creates a new ProgressReader
|
||||||
|
func NewProgressReader(rs io.ReadSeeker, total int64, onProgress func(read int64, total int64)) *ProgressReadSeeker {
|
||||||
|
prs := &ProgressReadSeeker{
|
||||||
|
reader: rs,
|
||||||
|
total: atomic.Int64{},
|
||||||
|
read: atomic.Int64{},
|
||||||
|
onProgress: onProgress,
|
||||||
|
}
|
||||||
|
prs.total.Store(total)
|
||||||
|
return prs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements io.Reader
|
||||||
|
func (pr *ProgressReadSeeker) Read(p []byte) (int, error) {
|
||||||
|
n, err := pr.reader.Read(p)
|
||||||
|
if n > 0 {
|
||||||
|
pr.read.Add(int64(n))
|
||||||
|
read := pr.read.Load()
|
||||||
|
|
||||||
|
if pr.onProgress != nil {
|
||||||
|
pr.onProgress(read, pr.total.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress returns the current progress as a float64 between 0 and 1
|
||||||
|
func (pr *ProgressReadSeeker) Progress() float64 {
|
||||||
|
if pr.total.Load() <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(pr.read.Load()) / float64(pr.total.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read returns the number of bytes read so far
|
||||||
|
func (pr *ProgressReadSeeker) BytesRead() int64 {
|
||||||
|
return pr.read.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total returns the total number of bytes
|
||||||
|
func (pr *ProgressReadSeeker) Total() int64 {
|
||||||
|
return pr.total.Load()
|
||||||
|
}
|
||||||
83
config/flags.go
Normal file
83
config/flags.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterFlags(cmd *cobra.Command) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
// 基础配置
|
||||||
|
flags.StringP("config", "c", "", "config file path")
|
||||||
|
flags.StringP("lang", "l", "", "language (e.g., zh-Hans, en)")
|
||||||
|
flags.IntP("workers", "w", 0, "number of workers")
|
||||||
|
flags.Int("retry", 0, "retry times")
|
||||||
|
flags.Int("threads", 0, "number of threads")
|
||||||
|
flags.Bool("stream", false, "enable stream mode")
|
||||||
|
flags.Bool("no-clean-cache", false, "do not clean cache on exit")
|
||||||
|
flags.String("proxy", "", "proxy URL (http, https, socks5, socks5h)")
|
||||||
|
|
||||||
|
// Telegram 配置
|
||||||
|
flags.String("telegram-token", "", "telegram bot token")
|
||||||
|
flags.Int("telegram-app-id", 0, "telegram app id")
|
||||||
|
flags.String("telegram-app-hash", "", "telegram app hash")
|
||||||
|
flags.Int("telegram-rpc-retry", 0, "telegram rpc retry times")
|
||||||
|
flags.Bool("telegram-userbot-enable", false, "enable userbot")
|
||||||
|
flags.String("telegram-userbot-session", "", "userbot session path")
|
||||||
|
flags.Bool("telegram-proxy-enable", false, "enable telegram proxy")
|
||||||
|
flags.String("telegram-proxy-url", "", "telegram proxy URL")
|
||||||
|
|
||||||
|
// 数据库配置
|
||||||
|
flags.String("db-path", "", "database path")
|
||||||
|
flags.String("db-session", "", "session database path")
|
||||||
|
|
||||||
|
// 临时目录配置
|
||||||
|
flags.String("temp-base-path", "", "temp directory base path")
|
||||||
|
|
||||||
|
// Parser 配置
|
||||||
|
flags.Bool("parser-plugin-enable", false, "enable parser plugins")
|
||||||
|
flags.StringSlice("parser-plugin-dirs", nil, "parser plugin directories")
|
||||||
|
flags.String("parser-proxy", "", "parser proxy URL")
|
||||||
|
|
||||||
|
// 绑定到 viper
|
||||||
|
bindFlags(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindFlags(cmd *cobra.Command) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
viper.BindPFlag("lang", flags.Lookup("lang"))
|
||||||
|
viper.BindPFlag("workers", flags.Lookup("workers"))
|
||||||
|
viper.BindPFlag("retry", flags.Lookup("retry"))
|
||||||
|
viper.BindPFlag("threads", flags.Lookup("threads"))
|
||||||
|
viper.BindPFlag("stream", flags.Lookup("stream"))
|
||||||
|
viper.BindPFlag("no_clean_cache", flags.Lookup("no-clean-cache"))
|
||||||
|
viper.BindPFlag("proxy", flags.Lookup("proxy"))
|
||||||
|
|
||||||
|
// Telegram
|
||||||
|
viper.BindPFlag("telegram.token", flags.Lookup("telegram-token"))
|
||||||
|
viper.BindPFlag("telegram.app_id", flags.Lookup("telegram-app-id"))
|
||||||
|
viper.BindPFlag("telegram.app_hash", flags.Lookup("telegram-app-hash"))
|
||||||
|
viper.BindPFlag("telegram.rpc_retry", flags.Lookup("telegram-rpc-retry"))
|
||||||
|
viper.BindPFlag("telegram.userbot.enable", flags.Lookup("telegram-userbot-enable"))
|
||||||
|
viper.BindPFlag("telegram.userbot.session", flags.Lookup("telegram-userbot-session"))
|
||||||
|
viper.BindPFlag("telegram.proxy.enable", flags.Lookup("telegram-proxy-enable"))
|
||||||
|
viper.BindPFlag("telegram.proxy.url", flags.Lookup("telegram-proxy-url"))
|
||||||
|
|
||||||
|
// database
|
||||||
|
viper.BindPFlag("db.path", flags.Lookup("db-path"))
|
||||||
|
viper.BindPFlag("db.session", flags.Lookup("db-session"))
|
||||||
|
// 临时目录
|
||||||
|
viper.BindPFlag("temp.base_path", flags.Lookup("temp-base-path"))
|
||||||
|
|
||||||
|
// Parser
|
||||||
|
viper.BindPFlag("parser.plugin_enable", flags.Lookup("parser-plugin-enable"))
|
||||||
|
viper.BindPFlag("parser.plugin_dirs", flags.Lookup("parser-plugin-dirs"))
|
||||||
|
viper.BindPFlag("parser.proxy", flags.Lookup("parser-proxy"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConfigFile(cmd *cobra.Command) string {
|
||||||
|
configFile, _ := cmd.Flags().GetString("config")
|
||||||
|
return configFile
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -11,8 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/duke-git/lancet/v2/slice"
|
"github.com/duke-git/lancet/v2/slice"
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n"
|
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
|
||||||
"github.com/krau/SaveAny-Bot/config/storage"
|
"github.com/krau/SaveAny-Bot/config/storage"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
@@ -52,16 +49,39 @@ func (c Config) GetStorageByName(name string) storage.StorageConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Init(ctx context.Context) error {
|
func Init(ctx context.Context, configFile ...string) error {
|
||||||
viper.SetConfigName("config")
|
|
||||||
viper.AddConfigPath(".")
|
|
||||||
viper.AddConfigPath("/etc/saveany/")
|
|
||||||
viper.SetConfigType("toml")
|
viper.SetConfigType("toml")
|
||||||
viper.SetEnvPrefix("SAVEANY")
|
viper.SetEnvPrefix("SAVEANY")
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
replacer := strings.NewReplacer(".", "_")
|
replacer := strings.NewReplacer(".", "_")
|
||||||
viper.SetEnvKeyReplacer(replacer)
|
viper.SetEnvKeyReplacer(replacer)
|
||||||
|
|
||||||
|
// 如果指定了配置文件路径,则使用指定的配置文件
|
||||||
|
// 配置文件支持传入一个 http(s) URL 地址
|
||||||
|
if len(configFile) > 0 && configFile[0] != "" {
|
||||||
|
cfg := configFile[0]
|
||||||
|
if strings.HasPrefix(cfg, "http://") || strings.HasPrefix(cfg, "https://") {
|
||||||
|
// 使用远程配置文件
|
||||||
|
resp, err := http.Get(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch remote config file: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("failed to fetch remote config file: status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if err := viper.ReadConfig(resp.Body); err != nil {
|
||||||
|
return fmt.Errorf("failed to read remote config file: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viper.SetConfigFile(cfg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
viper.AddConfigPath("/etc/saveany/")
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfigs := map[string]any{
|
defaultConfigs := map[string]any{
|
||||||
// 基础配置
|
// 基础配置
|
||||||
"lang": "zh-Hans",
|
"lang": "zh-Hans",
|
||||||
@@ -118,20 +138,11 @@ func Init(ctx context.Context) error {
|
|||||||
storageNames := make(map[string]struct{})
|
storageNames := make(map[string]struct{})
|
||||||
for _, storage := range cfg.Storages {
|
for _, storage := range cfg.Storages {
|
||||||
if _, ok := storageNames[storage.GetName()]; ok {
|
if _, ok := storageNames[storage.GetName()]; ok {
|
||||||
return errors.New(i18n.TWithoutInit(cfg.Lang, i18nk.ConfigErrDuplicateStorageName, map[string]any{
|
return fmt.Errorf("duplicate storage name: %s", storage.GetName())
|
||||||
"Name": storage.GetName(),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
storageNames[storage.GetName()] = struct{}{}
|
storageNames[storage.GetName()] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(i18n.TWithoutInit(cfg.Lang, i18nk.ConfigLoadedStorages, map[string]any{
|
|
||||||
"Count": len(cfg.Storages),
|
|
||||||
}))
|
|
||||||
for _, storage := range cfg.Storages {
|
|
||||||
fmt.Printf(" - %s (%s)\n", storage.GetName(), storage.GetType())
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Workers < 1 {
|
if cfg.Workers < 1 {
|
||||||
cfg.Workers = 1
|
cfg.Workers = 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
)
|
)
|
||||||
@@ -37,7 +39,7 @@ func (p *Progress) OnStart(ctx context.Context, info TaskInfo) {
|
|||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("开始执行批量下载任务\n总大小: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressBatchStartPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%.2f MB (%d个文件)", float64(info.TotalSize())/(1024*1024), info.Count())),
|
styling.Code(fmt.Sprintf("%.2f MB (%d个文件)", float64(info.TotalSize())/(1024*1024), info.Count())),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
@@ -78,22 +80,22 @@ func (p *Progress) OnProgress(ctx context.Context, info TaskInfo) {
|
|||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("正在处理批量下载任务\n总大小: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressBatchProcessingPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%.2f MB (%d个文件)", float64(info.TotalSize())/(1024*1024), info.Count())),
|
styling.Code(fmt.Sprintf("%.2f MB (%d个文件)", float64(info.TotalSize())/(1024*1024), info.Count())),
|
||||||
styling.Plain("\n正在处理:\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressProcessingListPrefix, nil)),
|
||||||
func() styling.StyledTextOption {
|
func() styling.StyledTextOption {
|
||||||
var lines []string
|
var lines []string
|
||||||
for _, elem := range info.Processing() {
|
for _, elem := range info.Processing() {
|
||||||
lines = append(lines, fmt.Sprintf(" - %s (%.2f MB)", elem.FileName(), float64(elem.FileSize())/(1024*1024)))
|
lines = append(lines, fmt.Sprintf(" - %s (%.2f MB)", elem.FileName(), float64(elem.FileSize())/(1024*1024)))
|
||||||
}
|
}
|
||||||
if len(lines) == 0 {
|
if len(lines) == 0 {
|
||||||
lines = append(lines, " - 无")
|
lines = append(lines, i18n.T(i18nk.BotMsgProgressProcessingNone, nil))
|
||||||
}
|
}
|
||||||
return styling.Plain(slice.Join(lines, "\n"))
|
return styling.Plain(slice.Join(lines, "\n"))
|
||||||
}(),
|
}(),
|
||||||
styling.Plain("\n平均速度: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressAvgSpeedPrefix, nil)),
|
||||||
styling.Bold(fmt.Sprintf("%.2f MB/s", dlutil.GetSpeed(info.Downloaded(), p.start)/(1024*1024))),
|
styling.Bold(fmt.Sprintf("%.2f MB/s", dlutil.GetSpeed(info.Downloaded(), p.start)/(1024*1024))),
|
||||||
styling.Plain("\n当前进度: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressCurrentProgressPrefix, nil)),
|
||||||
styling.Bold(fmt.Sprintf("%.2f%%", float64(info.Downloaded())/float64(info.TotalSize())*100)),
|
styling.Bold(fmt.Sprintf("%.2f%%", float64(info.Downloaded())/float64(info.TotalSize())*100)),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
@@ -133,19 +135,21 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
stylingErr = styling.Perform(&entityBuilder,
|
stylingErr = styling.Perform(&entityBuilder,
|
||||||
styling.Plain("任务已取消"),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressTaskCanceled, nil)),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
stylingErr = styling.Perform(&entityBuilder,
|
stylingErr = styling.Perform(&entityBuilder,
|
||||||
styling.Plain("处理失败, 错误:\n "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressTaskFailedWithError, map[string]any{
|
||||||
|
"Error": "",
|
||||||
|
})),
|
||||||
styling.Code(err.Error()),
|
styling.Code(err.Error()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stylingErr = styling.Perform(&entityBuilder,
|
stylingErr = styling.Perform(&entityBuilder,
|
||||||
styling.Plain("处理完成\n文件数: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressBatchDonePrefix, nil)),
|
||||||
styling.Code(strconv.Itoa(info.Count())),
|
styling.Code(strconv.Itoa(info.Count())),
|
||||||
styling.Plain("\n总大小: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressTotalSizePrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%.2f MB", float64(info.TotalSize())/(1024*1024))),
|
styling.Code(fmt.Sprintf("%.2f MB", float64(info.TotalSize())/(1024*1024))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
)
|
)
|
||||||
@@ -53,8 +55,10 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
ext := tgutil.ExtFromContext(ctx)
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
if ext != nil {
|
if ext != nil {
|
||||||
ext.EditMessage(p.chatID, &tg.MessagesEditMessageRequest{
|
ext.EditMessage(p.chatID, &tg.MessagesEditMessageRequest{
|
||||||
ID: p.msgID,
|
ID: p.msgID,
|
||||||
Message: fmt.Sprintf("处理已取消: %s", info.TaskID()),
|
Message: i18n.T(i18nk.BotMsgProgressTaskCanceledWithId, map[string]any{
|
||||||
|
"TaskID": info.TaskID(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -62,8 +66,10 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
ext := tgutil.ExtFromContext(ctx)
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
if ext != nil {
|
if ext != nil {
|
||||||
ext.EditMessage(p.chatID, &tg.MessagesEditMessageRequest{
|
ext.EditMessage(p.chatID, &tg.MessagesEditMessageRequest{
|
||||||
ID: p.msgID,
|
ID: p.msgID,
|
||||||
Message: fmt.Sprintf("处理失败: %s", err.Error()),
|
Message: i18n.T(i18nk.BotMsgProgressTaskFailedWithError, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,9 +79,9 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
|
|
||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("处理完成, 文件数量: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressDirectDonePrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%d", info.TotalFiles())),
|
styling.Code(fmt.Sprintf("%d", info.TotalFiles())),
|
||||||
styling.Plain("\n保存路径: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressSavePathPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Errorf("Failed to build entities: %s", err)
|
logger.Errorf("Failed to build entities: %s", err)
|
||||||
@@ -108,22 +114,22 @@ func (p *Progress) OnProgress(ctx context.Context, info TaskInfo) {
|
|||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("正在下载\n总大小: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressDownloadingPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%.2f MB (%d个文件)", float64(info.TotalBytes())/(1024*1024), info.TotalFiles())),
|
styling.Code(fmt.Sprintf("%.2f MB (%d个文件)", float64(info.TotalBytes())/(1024*1024), info.TotalFiles())),
|
||||||
styling.Plain("\n正在处理:\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressProcessingListPrefix, nil)),
|
||||||
func() styling.StyledTextOption {
|
func() styling.StyledTextOption {
|
||||||
var lines []string
|
var lines []string
|
||||||
for _, elem := range info.Processing() {
|
for _, elem := range info.Processing() {
|
||||||
lines = append(lines, fmt.Sprintf(" - %s (%.2f MB)", elem.FileName(), float64(elem.FileSize())/(1024*1024)))
|
lines = append(lines, fmt.Sprintf(" - %s (%.2f MB)", elem.FileName(), float64(elem.FileSize())/(1024*1024)))
|
||||||
}
|
}
|
||||||
if len(lines) == 0 {
|
if len(lines) == 0 {
|
||||||
lines = append(lines, " - 无")
|
lines = append(lines, i18n.T(i18nk.BotMsgProgressProcessingNone, nil))
|
||||||
}
|
}
|
||||||
return styling.Plain(slice.Join(lines, "\n"))
|
return styling.Plain(slice.Join(lines, "\n"))
|
||||||
}(),
|
}(),
|
||||||
styling.Plain("\n平均速度: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressAvgSpeedPrefix, nil)),
|
||||||
styling.Bold(fmt.Sprintf("%.2f MB/s", dlutil.GetSpeed(info.DownloadedBytes(), p.start)/(1024*1024))),
|
styling.Bold(fmt.Sprintf("%.2f MB/s", dlutil.GetSpeed(info.DownloadedBytes(), p.start)/(1024*1024))),
|
||||||
styling.Plain("\n当前进度: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressCurrentProgressPrefix, nil)),
|
||||||
styling.Bold(fmt.Sprintf("%.2f%%", float64(info.DownloadedBytes())/float64(info.TotalBytes())*100)),
|
styling.Bold(fmt.Sprintf("%.2f%%", float64(info.DownloadedBytes())/float64(info.TotalBytes())*100)),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
@@ -164,7 +170,10 @@ func (p *Progress) OnStart(ctx context.Context, info TaskInfo) {
|
|||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain(fmt.Sprintf("开始下载, 总大小: %.2f MB (%d 个文件)", float64(info.TotalBytes())/(1024*1024), info.TotalFiles()))); err != nil {
|
styling.Plain(i18n.T(i18nk.BotMsgProgressDirectStart, map[string]any{
|
||||||
|
"SizeMB": float64(info.TotalBytes()) / (1024 * 1024),
|
||||||
|
"Count": info.TotalFiles(),
|
||||||
|
}))); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
)
|
)
|
||||||
@@ -68,7 +70,9 @@ func (p *Progress) OnStart(ctx context.Context, info TaskInfo) {
|
|||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain(fmt.Sprintf("开始下载 %s 的资源\n总大小: ", info.Site())),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressParsedStartPrefix, map[string]any{
|
||||||
|
"Site": info.Site(),
|
||||||
|
})),
|
||||||
styling.Code(fmt.Sprintf("%.2f MB (%d个资源)", float64(info.TotalBytes())/(1024*1024), info.TotalResources())),
|
styling.Code(fmt.Sprintf("%.2f MB (%d个资源)", float64(info.TotalBytes())/(1024*1024), info.TotalResources())),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
@@ -109,22 +113,22 @@ func (p *Progress) OnProgress(ctx context.Context, info TaskInfo) {
|
|||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("正在下载\n总大小: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressDownloadingPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%.2f MB (%d个文件)", float64(info.TotalBytes())/(1024*1024), info.TotalResources())),
|
styling.Code(fmt.Sprintf("%.2f MB (%d个文件)", float64(info.TotalBytes())/(1024*1024), info.TotalResources())),
|
||||||
styling.Plain("\n正在处理:\n"),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressProcessingListPrefix, nil)),
|
||||||
func() styling.StyledTextOption {
|
func() styling.StyledTextOption {
|
||||||
var lines []string
|
var lines []string
|
||||||
for _, elem := range info.Processing() {
|
for _, elem := range info.Processing() {
|
||||||
lines = append(lines, fmt.Sprintf(" - %s (%.2f MB)", elem.FileName(), float64(elem.FileSize())/(1024*1024)))
|
lines = append(lines, fmt.Sprintf(" - %s (%.2f MB)", elem.FileName(), float64(elem.FileSize())/(1024*1024)))
|
||||||
}
|
}
|
||||||
if len(lines) == 0 {
|
if len(lines) == 0 {
|
||||||
lines = append(lines, " - 无")
|
lines = append(lines, i18n.T(i18nk.BotMsgProgressProcessingNone, nil))
|
||||||
}
|
}
|
||||||
return styling.Plain(slice.Join(lines, "\n"))
|
return styling.Plain(slice.Join(lines, "\n"))
|
||||||
}(),
|
}(),
|
||||||
styling.Plain("\n平均速度: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressAvgSpeedPrefix, nil)),
|
||||||
styling.Bold(fmt.Sprintf("%.2f MB/s", dlutil.GetSpeed(info.DownloadedBytes(), p.start)/(1024*1024))),
|
styling.Bold(fmt.Sprintf("%.2f MB/s", dlutil.GetSpeed(info.DownloadedBytes(), p.start)/(1024*1024))),
|
||||||
styling.Plain("\n当前进度: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressCurrentProgressPrefix, nil)),
|
||||||
styling.Bold(fmt.Sprintf("%.2f%%", float64(info.DownloadedBytes())/float64(info.TotalBytes())*100)),
|
styling.Bold(fmt.Sprintf("%.2f%%", float64(info.DownloadedBytes())/float64(info.TotalBytes())*100)),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
@@ -160,8 +164,10 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
ext := tgutil.ExtFromContext(ctx)
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
if ext != nil {
|
if ext != nil {
|
||||||
ext.EditMessage(p.ChatID, &tg.MessagesEditMessageRequest{
|
ext.EditMessage(p.ChatID, &tg.MessagesEditMessageRequest{
|
||||||
ID: p.MessageID,
|
ID: p.MessageID,
|
||||||
Message: fmt.Sprintf("处理已取消: %s", info.TaskID()),
|
Message: i18n.T(i18nk.BotMsgProgressTaskCanceledWithId, map[string]any{
|
||||||
|
"TaskID": info.TaskID(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -169,8 +175,10 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
ext := tgutil.ExtFromContext(ctx)
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
if ext != nil {
|
if ext != nil {
|
||||||
ext.EditMessage(p.ChatID, &tg.MessagesEditMessageRequest{
|
ext.EditMessage(p.ChatID, &tg.MessagesEditMessageRequest{
|
||||||
ID: p.MessageID,
|
ID: p.MessageID,
|
||||||
Message: fmt.Sprintf("处理失败: %s", err.Error()),
|
Message: i18n.T(i18nk.BotMsgProgressTaskFailedWithError, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,9 +188,9 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
|
|
||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("处理完成, 资源数量: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressParsedDonePrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%d", info.TotalResources())),
|
styling.Code(fmt.Sprintf("%d", info.TotalResources())),
|
||||||
styling.Plain("\n保存路径: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressSavePathPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Errorf("Failed to build entities: %s", err)
|
logger.Errorf("Failed to build entities: %s", err)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ func (p *Progress) OnStart(ctx context.Context, info TaskInfo) {
|
|||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("开始下载Telegraph\n图片数量: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressTelegraphStartPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%d", info.TotalPics())),
|
styling.Code(fmt.Sprintf("%d", info.TotalPics())),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
@@ -65,7 +67,7 @@ func (p *Progress) OnProgress(ctx context.Context, info TaskInfo) {
|
|||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("正在下载\n当前进度: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressTelegraphProgressPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%d/%d", info.Downloaded(), info.TotalPics())),
|
styling.Code(fmt.Sprintf("%d/%d", info.Downloaded(), info.TotalPics())),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
@@ -101,8 +103,10 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
ext := tgutil.ExtFromContext(ctx)
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
if ext != nil {
|
if ext != nil {
|
||||||
ext.EditMessage(p.ChatID, &tg.MessagesEditMessageRequest{
|
ext.EditMessage(p.ChatID, &tg.MessagesEditMessageRequest{
|
||||||
ID: p.MessageID,
|
ID: p.MessageID,
|
||||||
Message: fmt.Sprintf("处理已取消: %s", info.TaskID()),
|
Message: i18n.T(i18nk.BotMsgProgressTaskCanceledWithId, map[string]any{
|
||||||
|
"TaskID": info.TaskID(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -110,8 +114,10 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
ext := tgutil.ExtFromContext(ctx)
|
ext := tgutil.ExtFromContext(ctx)
|
||||||
if ext != nil {
|
if ext != nil {
|
||||||
ext.EditMessage(p.ChatID, &tg.MessagesEditMessageRequest{
|
ext.EditMessage(p.ChatID, &tg.MessagesEditMessageRequest{
|
||||||
ID: p.MessageID,
|
ID: p.MessageID,
|
||||||
Message: fmt.Sprintf("处理失败: %s", err.Error()),
|
Message: i18n.T(i18nk.BotMsgProgressTaskFailedWithError, map[string]any{
|
||||||
|
"Error": err.Error(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,9 +127,9 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
|
|
||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("处理完成\n图片数量: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressTelegraphDonePrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%d", info.TotalPics())),
|
styling.Code(fmt.Sprintf("%d", info.TotalPics())),
|
||||||
styling.Plain("\n保存路径: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressSavePathPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
logger.Errorf("Failed to build entities: %s", err)
|
logger.Errorf("Failed to build entities: %s", err)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"github.com/gotd/td/telegram/message/entity"
|
"github.com/gotd/td/telegram/message/entity"
|
||||||
"github.com/gotd/td/telegram/message/styling"
|
"github.com/gotd/td/telegram/message/styling"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
)
|
)
|
||||||
@@ -35,11 +37,11 @@ func (p *Progress) OnStart(ctx context.Context, info TaskInfo) {
|
|||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("开始下载\n文件名: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressFileStartPrefix, nil)),
|
||||||
styling.Code(info.FileName()),
|
styling.Code(info.FileName()),
|
||||||
styling.Plain("\n保存路径: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressSavePathPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
||||||
styling.Plain("\n文件大小: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressFileSizePrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%.2f MB", float64(info.FileSize())/(1024*1024))),
|
styling.Code(fmt.Sprintf("%.2f MB", float64(info.FileSize())/(1024*1024))),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
@@ -80,15 +82,15 @@ func (p *Progress) OnProgress(ctx context.Context, info TaskInfo, downloaded, to
|
|||||||
entityBuilder := entity.Builder{}
|
entityBuilder := entity.Builder{}
|
||||||
var entities []tg.MessageEntityClass
|
var entities []tg.MessageEntityClass
|
||||||
if err := styling.Perform(&entityBuilder,
|
if err := styling.Perform(&entityBuilder,
|
||||||
styling.Plain("正在处理下载任务\n文件名: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressFileProcessingPrefix, nil)),
|
||||||
styling.Code(info.FileName()),
|
styling.Code(info.FileName()),
|
||||||
styling.Plain("\n保存路径: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressSavePathPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
||||||
styling.Plain("\n文件大小: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressFileSizePrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%.2f MB", float64(total)/(1024*1024))),
|
styling.Code(fmt.Sprintf("%.2f MB", float64(total)/(1024*1024))),
|
||||||
styling.Plain("\n平均速度: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressAvgSpeedPrefix, nil)),
|
||||||
styling.Bold(fmt.Sprintf("%.2f MB/s", dlutil.GetSpeed(downloaded, p.start)/(1024*1024))),
|
styling.Bold(fmt.Sprintf("%.2f MB/s", dlutil.GetSpeed(downloaded, p.start)/(1024*1024))),
|
||||||
styling.Plain("\n当前进度: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressCurrentProgressPrefix, nil)),
|
||||||
styling.Bold(fmt.Sprintf("%.2f%%", float64(downloaded)/float64(total)*100)),
|
styling.Bold(fmt.Sprintf("%.2f%%", float64(downloaded)/float64(total)*100)),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
|
||||||
@@ -130,22 +132,24 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
stylingErr = styling.Perform(&entityBuilder,
|
stylingErr = styling.Perform(&entityBuilder,
|
||||||
styling.Plain("任务已取消\n文件名: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressTaskCanceled, nil)),
|
||||||
|
styling.Plain("\n"),
|
||||||
|
styling.Plain(i18n.T(i18nk.BotMsgProgressFileNamePrefix, nil)),
|
||||||
styling.Code(info.FileName()),
|
styling.Code(info.FileName()),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
stylingErr = styling.Perform(&entityBuilder,
|
stylingErr = styling.Perform(&entityBuilder,
|
||||||
styling.Plain("下载失败\n文件名: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressDownloadFailedPrefix, nil)),
|
||||||
styling.Code(info.FileName()),
|
styling.Code(info.FileName()),
|
||||||
styling.Plain("\n错误: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressErrorPrefix, nil)),
|
||||||
styling.Bold(err.Error()),
|
styling.Bold(err.Error()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stylingErr = styling.Perform(&entityBuilder,
|
stylingErr = styling.Perform(&entityBuilder,
|
||||||
styling.Plain("下载完成\n文件名: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressDownloadDonePrefix, nil)),
|
||||||
styling.Code(info.FileName()),
|
styling.Code(info.FileName()),
|
||||||
styling.Plain("\n保存路径: "),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressSavePathPrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
styling.Code(fmt.Sprintf("[%s]:%s", info.StorageName(), info.StoragePath())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func Init(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
logger.Debug("Database connected")
|
logger.Debug("Database connected")
|
||||||
if err := db.AutoMigrate(&User{}, &Dir{}, &Rule{}, &WatchChat{}); err != nil {
|
if err := db.AutoMigrate(&User{}, &Dir{}, &Rule{}, &WatchChat{}); err != nil {
|
||||||
logger.Fatal("迁移数据库失败, 如果您从旧版本升级, 建议手动删除数据库文件后重试: ", err)
|
logger.Fatal("Database migration failed; if upgrading from an old version, try deleting the database file and retrying", "error", err)
|
||||||
}
|
}
|
||||||
if err := syncUsers(ctx); err != nil {
|
if err := syncUsers(ctx); err != nil {
|
||||||
logger.Fatal("Failed to sync users:", err)
|
logger.Fatal("Failed to sync users:", err)
|
||||||
@@ -67,7 +67,7 @@ func syncUsers(ctx context.Context) error {
|
|||||||
if err := CreateUser(ctx, cfgID); err != nil {
|
if err := CreateUser(ctx, cfgID); err != nil {
|
||||||
return fmt.Errorf("failed to create user %d: %w", cfgID, err)
|
return fmt.Errorf("failed to create user %d: %w", cfgID, err)
|
||||||
}
|
}
|
||||||
logger.Infof("创建用户: %d", cfgID)
|
logger.Infof("Created user from config: %d", cfgID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ func syncUsers(ctx context.Context) error {
|
|||||||
if err := DeleteUser(ctx, &dbUser); err != nil {
|
if err := DeleteUser(ctx, &dbUser); err != nil {
|
||||||
return fmt.Errorf("failed to delete user %d: %w", dbID, err)
|
return fmt.Errorf("failed to delete user %d: %w", dbID, err)
|
||||||
}
|
}
|
||||||
logger.Infof("删除用户: %d", dbID)
|
logger.Infof("Deleted user not present in config: %d", dbID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,20 +11,22 @@ title: Introduction
|
|||||||
|
|
||||||
Save Any Bot is a tool that allows you to save files from Telegram to various storage backends.
|
Save Any Bot is a tool that allows you to save files from Telegram to various storage backends.
|
||||||
|
|
||||||
## Features
|
## 🎯 Features
|
||||||
|
|
||||||
- Supports documents/videos/images/stickers... and even Telegraph
|
- Supports documents/videos/images/stickers... and even [Telegraph](https://telegra.ph/)
|
||||||
- Breaks restrictions on saving files
|
- Breaks restrictions on saving files
|
||||||
- Batch download
|
- Batch download
|
||||||
- Streaming
|
- Streaming
|
||||||
- Multi-user
|
- Multi-user
|
||||||
- Automatic organization based on storage rules
|
- Automatic organization based on storage rules
|
||||||
|
- Watch specific chats and automatically save messages, with filters
|
||||||
|
- Write JS parser plugins to save files from almost any website
|
||||||
- Supports multiple storage backends:
|
- Supports multiple storage backends:
|
||||||
- Alist
|
- Alist
|
||||||
- S3
|
- S3
|
||||||
- WebDAV
|
- WebDAV
|
||||||
- Telegram (re-upload to specified chat)
|
|
||||||
- Local disk
|
- Local disk
|
||||||
|
- Telegram (re-upload to specified chat)
|
||||||
|
|
||||||
## [Contributors](https://github.com/krau/SaveAny-Bot/graphs/contributors)
|
## [Contributors](https://github.com/krau/SaveAny-Bot/graphs/contributors)
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,33 @@ weight: 20
|
|||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
|
Before you start, please fork this repository, clone it locally, and set up your Go development environment.
|
||||||
|
|
||||||
|
Here are some guidelines and suggestions for contributing code. You don't have to strictly follow them, but they help speed up review and merging:
|
||||||
|
|
||||||
|
- **Open an issue before adding new features**, so we can discuss design and implementation details and avoid work that doesn't fit the project design.
|
||||||
|
- **Use modern development tools**, format your code before committing, and keep the style consistent.
|
||||||
|
- **Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)**, and avoid vague or overly simple commit messages.
|
||||||
|
|
||||||
## Contributing New Storage Backend
|
## Contributing New Storage Backend
|
||||||
|
|
||||||
1. Fork this repository and clone it to your local machine.
|
1. Add the new storage backend type in `pkg/enums/storage/storages.go` and run code generation.
|
||||||
2. Add the new storage backend type in `pkg/enums/storage/storages.go` and run code generation.
|
2. Define the storage backend configuration in the `config/storage` directory and add it to `config/storage/factory.go`.
|
||||||
3. Define the storage backend configuration in the `config/storage` directory and add it to `config/storage/factory.go`.
|
3. Create a new package in the `storage` directory, implement the storage backend, and import it in `storage/storage.go`.
|
||||||
4. Create a new package in the `storage` directory, implement the storage backend, and import it in `storage/storage.go`.
|
4. Update the documentation to include configuration details for the new storage backend.
|
||||||
5. Update the documentation to include configuration details for the new storage backend.
|
|
||||||
|
## Contributing New Parsers
|
||||||
|
|
||||||
|
You can either implement native parsers in Go (recommended), or write JavaScript-based parser plugins.
|
||||||
|
|
||||||
|
If you use Go:
|
||||||
|
|
||||||
|
1. Create a new package under the `parsers` directory and implement the parser logic.
|
||||||
|
2. Register the parser in the `init` function in `parsers/parser.go`.
|
||||||
|
|
||||||
|
If you use JavaScript:
|
||||||
|
|
||||||
|
1. Refer to `plugins/example_parser_basic.js` as an example.
|
||||||
|
2. Create a new `.js` file in the `plugins` directory and implement your parsing logic there.
|
||||||
|
|
||||||
|
Note: Parsers under the `plugins` directory are not embedded into the binary by default. Users must download them manually and place them in the configured plugin directories to enable them.
|
||||||
@@ -44,6 +44,15 @@ Stream mode is very useful for deployment environments with limited disk space,
|
|||||||
- `workers`: Number of tasks to process simultaneously, default is 3.
|
- `workers`: Number of tasks to process simultaneously, default is 3.
|
||||||
- `threads`: Number of threads used when downloading files, default is 4. Only effective when Stream mode is not enabled.
|
- `threads`: Number of threads used when downloading files, default is 4. Only effective when Stream mode is not enabled.
|
||||||
- `retry`: Number of retries when a task fails, default is 3.
|
- `retry`: Number of retries when a task fails, default is 3.
|
||||||
|
- `proxy`: Global proxy configuration. After setting this, all network connections inside the program will try to use this proxy. Optional.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
stream = false
|
||||||
|
workers = 3
|
||||||
|
threads = 4
|
||||||
|
retry = 3
|
||||||
|
proxy = "socks5://127.0.0.1:7890"
|
||||||
|
```
|
||||||
|
|
||||||
### Telegram Configuration
|
### Telegram Configuration
|
||||||
|
|
||||||
@@ -54,6 +63,17 @@ Stream mode is very useful for deployment environments with limited disk space,
|
|||||||
- `proxy`: Proxy configuration, optional.
|
- `proxy`: Proxy configuration, optional.
|
||||||
- `enable`: Whether to enable the proxy.
|
- `enable`: Whether to enable the proxy.
|
||||||
- `url`: Proxy address, only supports `socks5://`
|
- `url`: Proxy address, only supports `socks5://`
|
||||||
|
- `userbot`: Userbot configuration, optional.
|
||||||
|
- `enable`: Enable userbot integration. Requires logging in with a user account; you should use your own API ID & Hash when enabling this.
|
||||||
|
- `session`: Path to the userbot session file, default is `data/usersession.db`.
|
||||||
|
|
||||||
|
{{< hint warning >}}
|
||||||
|
After enabling userbot integration, the bot can download files from private channels and groups, but there is an unavoidable risk of the account being banned.
|
||||||
|
<br />
|
||||||
|
On the first start after enabling userbot, you need to input phone number, 2FA and verification code in the terminal.
|
||||||
|
<br />
|
||||||
|
If you deploy with Docker, please run the container with `-it` for an interactive environment, then perform the login.
|
||||||
|
{{< /hint >}}
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[telegram]
|
[telegram]
|
||||||
@@ -65,6 +85,9 @@ rpc_retry = 5
|
|||||||
[telegram.proxy]
|
[telegram.proxy]
|
||||||
enable = false
|
enable = false
|
||||||
url = "socks5://127.0.0.1:7890"
|
url = "socks5://127.0.0.1:7890"
|
||||||
|
[telegram.userbot]
|
||||||
|
enable = false
|
||||||
|
session = "data/usersession.db"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Storage Endpoints List
|
### Storage Endpoints List
|
||||||
@@ -131,6 +154,39 @@ storages = []
|
|||||||
blacklist = true
|
blacklist = true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
Event hooks allow you to run custom commands based on task status while the bot is processing tasks. Currently only arbitrary command execution is supported, configured via `[hook.exec]`.
|
||||||
|
|
||||||
|
Supported event types:
|
||||||
|
|
||||||
|
- `task_before_start`: Before a task starts
|
||||||
|
- `task_success`: After a task completes successfully
|
||||||
|
- `task_fail`: After a task fails
|
||||||
|
- `task_cancel`: After a task is cancelled
|
||||||
|
|
||||||
|
The configured value must be a full shell command line. The bot will execute this command when the event occurs. Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[hook.exec]
|
||||||
|
task_before_start = "echo 'task is about to start'"
|
||||||
|
task_success = "bash /path/to/success_script.sh"
|
||||||
|
task_fail = "curl -X POST https://example.com/api/notify -d 'task failed'"
|
||||||
|
task_cancel = "bash /path/to/cancel_script.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parsers
|
||||||
|
|
||||||
|
Parsers give the bot the ability to handle non-Telegram files, such as downloading files from other websites. Configure them via `[parsers]`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[parsers]
|
||||||
|
plugin_enable = true # Whether to enable parser plugins
|
||||||
|
plugin_dirs = ["./plugins"] # Plugin directories, can be multiple
|
||||||
|
```
|
||||||
|
|
||||||
|
The above settings only control JavaScript-based parser plugins. The bot also has built-in parsers implemented in Go, which are enabled by default.
|
||||||
|
|
||||||
### Miscellaneous
|
### Miscellaneous
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
|||||||
@@ -46,15 +46,29 @@ base_path = "/path/to/webdav" # Base path in WebDAV, all files will be stored un
|
|||||||
`type=s3`
|
`type=s3`
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
endpoint = "s3.example.com" # Endpoint for S3
|
endpoint = "s3.example.com" # Endpoint for S3, defaults to AWS S3 endpoint if not set
|
||||||
region = "us-east-1" # Region for S3
|
region = "us-east-1" # Region for S3
|
||||||
access_key_id = "your_access_key_id" # Access key ID for S3
|
access_key_id = "your_access_key_id" # Access key ID for S3
|
||||||
secret_access_key = "your_secret_access_key" # Secret access key for S3
|
secret_access_key = "your_secret_access_key" # Secret access key for S3
|
||||||
bucket_name = "your_bucket_name" # Bucket name for S3
|
bucket_name = "your_bucket_name" # Bucket name for S3
|
||||||
use_ssl = true # Whether to use SSL, default is true
|
|
||||||
base_path = "/path/to/s3" # Base path in S3, all files will be stored under this path
|
base_path = "/path/to/s3" # Base path in S3, all files will be stored under this path
|
||||||
|
virtual_host = false # Use virtual-host style URL, default is false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example of virtual-host-style URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://your_bucket_name.s3.example.com/path/to/s3/your_file
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of path-style URL (when `virtual_host` is false):
|
||||||
|
|
||||||
|
```
|
||||||
|
https://s3.example.com/your_bucket_name/path/to/s3/your_file
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using a third-party S3-compatible service, it usually uses path-style URLs. AWS S3 typically uses virtual-host-style URLs. Please refer to your S3-compatible service documentation for details.
|
||||||
|
|
||||||
## Telegram
|
## Telegram
|
||||||
|
|
||||||
`type=telegram`
|
`type=telegram`
|
||||||
@@ -62,5 +76,8 @@ base_path = "/path/to/s3" # Base path in S3, all files will be stored under this
|
|||||||
Stream mode is not supported.
|
Stream mode is not supported.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
chat_id = "123456789" # Telegram chat ID, the Bot will send files to this chat
|
chat_id = "123456789" # Telegram chat ID, the bot will send files to this chat
|
||||||
|
force_file = false # Force sending as file, default is false
|
||||||
|
skip_large = false # Skip large files, default is false. If enabled, files exceeding Telegram's limit will not be uploaded.
|
||||||
|
spilt_size_mb = 2000 # Split size in MB, default is 2000 MB (2 GB). Files larger than this will be split into multiple parts (zip format). Ignored when skip_large is true.
|
||||||
```
|
```
|
||||||
@@ -4,7 +4,7 @@ title: "Installation and Updates"
|
|||||||
|
|
||||||
# Installation and Updates
|
# Installation and Updates
|
||||||
|
|
||||||
## Deploy from Pre-compiled Files
|
## Deploy from Pre-compiled Files (Recommended)
|
||||||
|
|
||||||
Download the binary file for your platform from the [Release](https://github.com/krau/SaveAny-Bot/releases) page.
|
Download the binary file for your platform from the [Release](https://github.com/krau/SaveAny-Bot/releases) page.
|
||||||
|
|
||||||
@@ -129,17 +129,41 @@ docker run -d --name saveany-bot \
|
|||||||
ghcr.io/krau/saveany-bot:latest
|
ghcr.io/krau/saveany-bot:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
{{< hint info >}}
|
||||||
|
About Docker image variants
|
||||||
|
<br />
|
||||||
|
<ul>
|
||||||
|
<li>Default: Includes all features and dependencies, larger in size. Use this if you don't have special requirements.</li>
|
||||||
|
<li>micro: Slimmed-down image with some optional dependencies removed, smaller in size.</li>
|
||||||
|
<li>pico: Minimal image containing only core features, smallest in size.</li>
|
||||||
|
</ul>
|
||||||
|
You can pull different variants by specifying tags, for example: <code>ghcr.io/krau/saveany-bot:micro</code>
|
||||||
|
<br />
|
||||||
|
For more details about the variants, see the Dockerfile in the project root.
|
||||||
|
{{< /hint >}}
|
||||||
|
|
||||||
## Updates
|
## Updates
|
||||||
|
|
||||||
Use `upgrade` or `up` to upgrade to the latest version
|
If you deployed from pre-compiled binaries, use the following CLI command to update:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./saveany-bot upgrade
|
./saveany-bot up
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(`upgrade` is also available as an alias.)
|
||||||
|
|
||||||
If you deployed with Docker, use the following commands to update:
|
If you deployed with Docker, use the following commands to update:
|
||||||
|
|
||||||
|
docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/krau/saveany-bot:latest
|
docker pull ghcr.io/krau/saveany-bot:latest
|
||||||
docker restart saveany-bot
|
docker restart saveany-bot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
docker compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
docker compose restart
|
||||||
|
```
|
||||||
@@ -5,16 +5,17 @@ weight: 10
|
|||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
|
This page introduces some of Save Any Bot's features and basic usage. If you can't find what you need here, please also see the [Configuration Guide](../deployment/configuration) or ask in GitHub [Discussions](https://github.com/krau/SaveAny-Bot/discussions).
|
||||||
|
|
||||||
## File Transfer
|
## File Transfer
|
||||||
|
|
||||||
The bot accepts two types of messages: files and links.
|
To use the bot's Telegram file saving feature, you need to send or forward the following types of messages to the bot:
|
||||||
|
|
||||||
Supported links:
|
1. File or media messages, such as images, videos, documents, etc.
|
||||||
|
2. Telegram message links, for example: `https://t.me/acherkrau/1097`. **Even if the channel prohibits forwarding and saving, the bot can still download its files.**
|
||||||
|
3. Telegra.ph article links. The bot will download all images in the article.
|
||||||
|
|
||||||
1. Telegram message links, for example: `https://t.me/acherkrau/1097`. **Even if the channel prohibits forwarding and saving, the bot can still download its files.**
|
## Silent Mode (silent)
|
||||||
2. Telegra.ph article links, the bot will download all images within.
|
|
||||||
|
|
||||||
## Silent Mode
|
|
||||||
|
|
||||||
Use the `/silent` command to toggle silent mode.
|
Use the `/silent` command to toggle silent mode.
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ Before enabling silent mode, you need to set the default save location using the
|
|||||||
|
|
||||||
## Storage Rules
|
## Storage Rules
|
||||||
|
|
||||||
Allows you to set some redirection rules for the bot when uploading files to storage, for automatic organization of saved files.
|
Storage rules allow you to define redirection rules when the bot uploads files to storage, so that saved files are automatically organized.
|
||||||
|
|
||||||
See: <a href="https://github.com/krau/SaveAny-Bot/issues/28" target="_blank">#28</a>
|
See: <a href="https://github.com/krau/SaveAny-Bot/issues/28" target="_blank">#28</a>
|
||||||
|
|
||||||
@@ -35,20 +36,21 @@ Currently supported rule types:
|
|||||||
|
|
||||||
1. FILENAME-REGEX
|
1. FILENAME-REGEX
|
||||||
2. MESSAGE-REGEX
|
2. MESSAGE-REGEX
|
||||||
|
3. IS-ALBUM
|
||||||
|
|
||||||
Basic syntax for adding rules:
|
Basic syntax for adding rules:
|
||||||
|
|
||||||
"Rule Type Rule Content Storage Name Path"
|
"RuleType RuleContent StorageName Path"
|
||||||
|
|
||||||
Pay attention to the use of spaces; the bot can only parse correctly formatted syntax. Below is an example of a valid rule command:
|
Pay attention to spaces; the bot can only parse correctly formatted syntax. Below is an example of a valid rule command:
|
||||||
|
|
||||||
```
|
```
|
||||||
/rule add FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /videos
|
/rule add FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /videos
|
||||||
```
|
```
|
||||||
|
|
||||||
Additionally, if "CHOSEN" is used as the storage name in the rule, it means the file will be stored in the path of the storage selected via button click.
|
In addition, if `CHOSEN` is used as the storage name in the rule, it means files will be stored under the path of the storage you selected by clicking the inline button.
|
||||||
|
|
||||||
Rule descriptions:
|
Rule types:
|
||||||
|
|
||||||
### FILENAME-REGEX
|
### FILENAME-REGEX
|
||||||
|
|
||||||
@@ -58,8 +60,65 @@ Matches based on filename regex. The rule content must be a valid regular expres
|
|||||||
FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /videos
|
FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /videos
|
||||||
```
|
```
|
||||||
|
|
||||||
This means files with extensions mp4, mkv, ts, avi, flv will be saved to the /videos directory in the storage named MyAlist (also affected by the `base_path` in the configuration file).
|
This means files with extensions mp4, mkv, ts, avi, flv will be saved to the `/videos` directory in the storage named `MyAlist` (also affected by the `base_path` in the configuration file).
|
||||||
|
|
||||||
### MESSAGE-REGEX
|
### MESSAGE-REGEX
|
||||||
|
|
||||||
Similar to the above, but matches based on the text content of the message itself.
|
Similar to the above, but matches based on the text content of the message itself.
|
||||||
|
|
||||||
|
### IS-ALBUM
|
||||||
|
|
||||||
|
Matches album messages (media groups). Rule content can only be `true` or `false`.
|
||||||
|
|
||||||
|
If the path in the rule uses `NEW-FOR-ALBUM`, the bot will create a new folder for each media group and store all files of that group there. See: https://github.com/krau/SaveAny-Bot/issues/87
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
IS-ALBUM true MyWebdav NEW-FOR-ALBUM
|
||||||
|
```
|
||||||
|
|
||||||
|
This will save media-group messages to the storage named `MyWebdav`, creating a new folder (generated from the first file) for each album.
|
||||||
|
|
||||||
|
## Watch Chats
|
||||||
|
|
||||||
|
{{< hint warning >}}
|
||||||
|
This feature requires enabling UserBot integration.
|
||||||
|
{{< /hint >}}
|
||||||
|
|
||||||
|
You can watch messages in a specific chat and automatically save them to the default storage, following storage rules. You can also add filters so that only matching messages are saved.
|
||||||
|
|
||||||
|
Watch a chat:
|
||||||
|
|
||||||
|
```
|
||||||
|
/watch <chat_id/username> [filter]
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop watching:
|
||||||
|
|
||||||
|
```
|
||||||
|
/unwatch <chat_id/username>
|
||||||
|
```
|
||||||
|
|
||||||
|
Filter types:
|
||||||
|
|
||||||
|
### msgre
|
||||||
|
|
||||||
|
Regex-match the message text. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
/watch 12345678 msgre:.*hello.*
|
||||||
|
```
|
||||||
|
|
||||||
|
This will watch the chat with ID `12345678`, and only save messages whose text contains `hello`.
|
||||||
|
|
||||||
|
## Save Files Outside Telegram
|
||||||
|
|
||||||
|
Besides files on Telegram, the bot can also save files from other websites via JavaScript plugins or built-in parsers.
|
||||||
|
|
||||||
|
> See the [Contributing Parsers](../contribute) document for details.
|
||||||
|
|
||||||
|
Just send links that match the requirements of a parser to the bot. Currently built-in parsers include:
|
||||||
|
|
||||||
|
- Twitter
|
||||||
|
- Kemono
|
||||||
@@ -5,7 +5,7 @@ weight: 20
|
|||||||
|
|
||||||
# 参与开发
|
# 参与开发
|
||||||
|
|
||||||
在开始之前, 请 Fork 本项目, 并克隆到本地, 并确保 Go 版本 >= 1.23.
|
在开始之前, 请 Fork 本项目, 并克隆到本地, 安装好 Go 开发环境.
|
||||||
|
|
||||||
以下是一些贡献代码的指南或建议, 你不必完全遵守, 但将有助于快速 review 并合并你的提交:
|
以下是一些贡献代码的指南或建议, 你不必完全遵守, 但将有助于快速 review 并合并你的提交:
|
||||||
|
|
||||||
|
|||||||
@@ -76,8 +76,14 @@ https://s3.example.com/your_bucket_name/path/to/s3/your_file
|
|||||||
不支持 Stream 模式.
|
不支持 Stream 模式.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
chat_id = "123456789" # Telegram 聊天 ID, Bot 将把文件发送到这个聊天
|
# Telegram 聊天 ID, Bot 将把文件发送到这个聊天
|
||||||
force_file = false # 是否强制使用文件方式发送, 默认为 false.
|
chat_id = "123456789"
|
||||||
skip_large = true # 是否跳过大文件, 默认为 true. 如果启用, 超过 Telegram 限制的文件将不会上传.
|
# 是否强制使用文件方式发送, 默认为 false
|
||||||
spilt_size_mb = 2000 # 分卷大小, 单位 MB, 默认为 2000 MB (2 GB). 超过该大小的文件将被分割成多个部分上传.(使用 zip 格式)
|
force_file = false
|
||||||
|
# 是否跳过大文件, 默认为 false. 如果启用, 超过 Telegram 限制的文件将不会上传.
|
||||||
|
skip_large = false
|
||||||
|
# 分卷大小, 单位 MB, 默认为 2000 MB (2 GB).
|
||||||
|
# 超过该大小的文件将被分割成多个部分上传.(使用 zip 格式)
|
||||||
|
# 当 skip_large 启用时, 该选项无效.
|
||||||
|
spilt_size_mb = 2000
|
||||||
```
|
```
|
||||||
11
go.mod
11
go.mod
@@ -6,7 +6,11 @@ require (
|
|||||||
github.com/blang/semver v3.5.1+incompatible
|
github.com/blang/semver v3.5.1+incompatible
|
||||||
github.com/celestix/gotgproto v1.0.0-beta22
|
github.com/celestix/gotgproto v1.0.0-beta22
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10
|
github.com/gabriel-vasile/mimetype v1.4.10
|
||||||
github.com/goccy/go-yaml v1.18.0
|
github.com/goccy/go-yaml v1.18.0
|
||||||
github.com/gotd/contrib v0.21.1
|
github.com/gotd/contrib v0.21.1
|
||||||
@@ -31,7 +35,7 @@ require (
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.10.2 // indirect
|
github.com/charmbracelet/x/ansi v0.10.2 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
@@ -39,7 +43,7 @@ require (
|
|||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
github.com/deckarep/golang-set/v2 v2.7.0 // indirect
|
github.com/deckarep/golang-set/v2 v2.7.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
@@ -67,9 +71,12 @@ require (
|
|||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/ncruces/julianday v1.0.0 // indirect
|
github.com/ncruces/julianday v1.0.0 // indirect
|
||||||
|
|||||||
15
go.sum
15
go.sum
@@ -42,8 +42,14 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc=
|
github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc=
|
||||||
github.com/cevatbarisyilmaz/ara v0.0.4/go.mod h1:BfFOxnUd6Mj6xmcvRxHN3Sr21Z1T3U2MYkYOmoQe4Ts=
|
github.com/cevatbarisyilmaz/ara v0.0.4/go.mod h1:BfFOxnUd6Mj6xmcvRxHN3Sr21Z1T3U2MYkYOmoQe4Ts=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
||||||
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
|
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
|
||||||
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
|
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
@@ -76,6 +82,8 @@ github.com/duke-git/lancet/v2 v2.3.7 h1:nnNBA9KyoqwbPm4nFmEFVIbXeAmpqf6IDCH45+HH
|
|||||||
github.com/duke-git/lancet/v2 v2.3.7/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
github.com/duke-git/lancet/v2 v2.3.7/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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
@@ -168,6 +176,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
|||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||||
@@ -180,6 +190,10 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
|
|||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
|
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
|
||||||
@@ -303,6 +317,7 @@ golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|||||||
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import (
|
|||||||
var UserStorages = make(map[int64][]Storage)
|
var UserStorages = make(map[int64][]Storage)
|
||||||
|
|
||||||
// GetStorageByName returns storage by name from cache or creates new one
|
// GetStorageByName returns storage by name from cache or creates new one
|
||||||
func getStorageByName(ctx context.Context, name string) (Storage, error) {
|
// It should NOT be used to get storage for user, use GetStorageByUserIDAndName instead
|
||||||
|
func GetStorageByName(ctx context.Context, name string) (Storage, error) {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return nil, ErrStorageNameEmpty
|
return nil, ErrStorageNameEmpty
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ func GetStorageByUserIDAndName(ctx context.Context, chatID int64, name string) (
|
|||||||
return nil, fmt.Errorf("no storage %s for user %d", name, chatID)
|
return nil, fmt.Errorf("no storage %s for user %d", name, chatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getStorageByName(ctx, name)
|
return GetStorageByName(ctx, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserStorages(ctx context.Context, chatID int64) []Storage {
|
func GetUserStorages(ctx context.Context, chatID int64) []Storage {
|
||||||
@@ -55,7 +56,7 @@ func GetUserStorages(ctx context.Context, chatID int64) []Storage {
|
|||||||
}
|
}
|
||||||
var storages []Storage
|
var storages []Storage
|
||||||
for _, name := range config.C().GetStorageNamesByUserID(chatID) {
|
for _, name := range config.C().GetStorageNamesByUserID(chatID) {
|
||||||
storage, err := getStorageByName(ctx, name)
|
storage, err := GetStorageByName(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -66,14 +67,14 @@ func GetUserStorages(ctx context.Context, chatID int64) []Storage {
|
|||||||
|
|
||||||
func LoadStorages(ctx context.Context) {
|
func LoadStorages(ctx context.Context) {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
logger.Info("加载存储...")
|
logger.Debug("loading storages...")
|
||||||
for _, storage := range config.C().Storages {
|
for _, storage := range config.C().Storages {
|
||||||
_, err := getStorageByName(ctx, storage.GetName())
|
_, err := GetStorageByName(ctx, storage.GetName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("加载存储 %s 失败: %v", storage.GetName(), err)
|
logger.Errorf("failed to load storage %s: %v", storage.GetName(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.Infof("成功加载 %d 个存储", len(Storages))
|
logger.Infof("successfully loaded %d storages", len(Storages))
|
||||||
for user := range config.C().GetUsersID() {
|
for user := range config.C().GetUsersID() {
|
||||||
UserStorages[int64(user)] = GetUserStorages(ctx, int64(user))
|
UserStorages[int64(user)] = GetUserStorages(ctx, int64(user))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
|
// Init 只应该在创建存储时调用一次
|
||||||
Init(ctx context.Context, cfg storcfg.StorageConfig) error
|
Init(ctx context.Context, cfg storcfg.StorageConfig) error
|
||||||
Type() storenum.StorageType
|
Type() storenum.StorageType
|
||||||
Name() string
|
Name() string
|
||||||
@@ -42,15 +43,16 @@ var storageConstructors = map[storenum.StorageType]StorageConstructor{
|
|||||||
storenum.Telegram: func() Storage { return new(telegram.Telegram) },
|
storenum.Telegram: func() Storage { return new(telegram.Telegram) },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewStorage creates a new storage instance based on the provided config and initializes it
|
||||||
func NewStorage(ctx context.Context, cfg storcfg.StorageConfig) (Storage, error) {
|
func NewStorage(ctx context.Context, cfg storcfg.StorageConfig) (Storage, error) {
|
||||||
constructor, ok := storageConstructors[cfg.GetType()]
|
constructor, ok := storageConstructors[cfg.GetType()]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("不支持的存储类型: %s", cfg.GetType())
|
return nil, fmt.Errorf("unsupported storage type: %s", cfg.GetType())
|
||||||
}
|
}
|
||||||
|
|
||||||
storage := constructor()
|
storage := constructor()
|
||||||
if err := storage.Init(ctx, cfg); err != nil {
|
if err := storage.Init(ctx, cfg); err != nil {
|
||||||
return nil, fmt.Errorf("初始化 %s 存储失败: %w", cfg.GetName(), err)
|
return nil, fmt.Errorf("failed to initialize storage %s: %w", cfg.GetName(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return storage, nil
|
return storage, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user