Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
336309fad0 | ||
|
|
394cdff865 | ||
|
|
40cb3dad9d | ||
|
|
2979628cf7 | ||
|
|
c82c2462bf | ||
|
|
88128ecac2 | ||
|
|
758564d436 | ||
|
|
f5e33472eb | ||
|
|
4df2c5a06d | ||
|
|
eb6f8675a4 | ||
|
|
473a5b9413 | ||
|
|
6c2abe3025 | ||
|
|
e7e5b9f434 | ||
|
|
d4d39d1c07 | ||
|
|
73b5f1b18e | ||
|
|
837700bf63 | ||
|
|
53e6d7cc54 | ||
|
|
4206d1fe96 | ||
|
|
6566dbbf96 |
30
.github/workflows/docs.yml
vendored
30
.github/workflows/docs.yml
vendored
@@ -6,17 +6,31 @@ on:
|
||||
paths:
|
||||
- "docs/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- uses: actions/cache@v4
|
||||
submodules: true # Fetch Hugo themes (true OR recursive)
|
||||
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v3
|
||||
with:
|
||||
key: ${{ github.ref }}
|
||||
path: .cache
|
||||
- run: pip install mkdocs-material
|
||||
- run: cd docs && mkdocs gh-deploy --force
|
||||
hugo-version: '0.147.8'
|
||||
extended: true
|
||||
|
||||
- name: Build
|
||||
run: hugo --minify --destination public --source docs
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs/public
|
||||
publish_branch: gh-pages
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,4 +6,5 @@ downloads/
|
||||
session.*
|
||||
cache.db
|
||||
.vscode/
|
||||
temp/
|
||||
temp/
|
||||
.hugo_build.lock
|
||||
32
README.md
32
README.md
@@ -1,28 +1,39 @@
|
||||
<div align="center">
|
||||
|
||||
# <img src="docs/logo.jpg" width="45" align="center"> Save Any Bot
|
||||
# <img src="docs/static/logo.png" width="45" align="center"> Save Any Bot
|
||||
|
||||
**简体中文** | [English](README_EN.md)
|
||||
**简体中文** | [English](https://sabot.unv.app/en/)
|
||||
|
||||
把 Telegram 的文件保存到各类存储端.
|
||||
|
||||
> _就像 PikPak Bot 一样_
|
||||
把 Telegram 上的文件转存到多种存储端.
|
||||
|
||||
</div>
|
||||
|
||||
## [部署](https://sabot.unv.app/deploy/)
|
||||
## 部署
|
||||
|
||||
## [参与开发](https://sabot.unv.app/contribute/)
|
||||
请参考 [部署文档](https://sabot.unv.app/deployment/installation/)
|
||||
|
||||
---
|
||||
## Features
|
||||
|
||||
## 赞助
|
||||
- 支持文档/视频/图片/贴纸… 甚至还有 Telegraph
|
||||
- 破解禁止保存的文件
|
||||
- 批量下载
|
||||
- 流式传输
|
||||
- 多用户
|
||||
- 基于存储规则的自动整理
|
||||
- 支持多种存储端:
|
||||
- Alist
|
||||
- Minio (S3 兼容)
|
||||
- WebDAV
|
||||
- Telegram (重传回指定聊天)
|
||||
- 本地磁盘
|
||||
|
||||
## Sponsors
|
||||
|
||||
本项目受到 [YxVM](https://yxvm.com/) 与 [NodeSupport](https://github.com/NodeSeekDev/NodeSupport) 的支持.
|
||||
|
||||
如果这个项目对你有帮助, 你可以考虑通过以下方式赞助我:
|
||||
|
||||
- [爱发电](https://afdian.com/a/acherkrau)
|
||||
- [爱发电](https://afdian.com/a/unvapp)
|
||||
|
||||
## Contributors
|
||||
|
||||
@@ -68,4 +79,5 @@
|
||||
- [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
|
||||
|
||||
108
README_EN.md
108
README_EN.md
@@ -1,108 +0,0 @@
|
||||
<div align="center">
|
||||
|
||||
# <img src="docs/logo.jpg" width="45" align="center"> Save Any Bot
|
||||
|
||||
[简体中文](README.md) | **English**
|
||||
|
||||
Save Telegram files to various storage endpoints.
|
||||
|
||||
> _Just like PikPak Bot_
|
||||
|
||||
</div>
|
||||
|
||||
## Deployment
|
||||
|
||||
### Deploy from Binary
|
||||
|
||||
Download the binary file for your platform from the [Release](https://github.com/krau/SaveAny-Bot/releases) page.
|
||||
|
||||
Create a `config.toml` file in the extracted directory, refer to [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) for configuration.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
chmod +x saveany-bot
|
||||
./saveany-bot
|
||||
```
|
||||
|
||||
#### Add as systemd Service
|
||||
|
||||
Create file `/etc/systemd/system/saveany-bot.service` and write the following content:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=SaveAnyBot
|
||||
After=systemd-user-sessions.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/yourpath/
|
||||
ExecStart=/yourpath/saveany-bot
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Enable auto-start and start the service:
|
||||
|
||||
```bash
|
||||
systemctl enable --now saveany-bot
|
||||
```
|
||||
|
||||
### Deploy with Docker
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
Download [docker-compose.yml](https://github.com/krau/SaveAny-Bot/blob/main/docker-compose.yml) file and create a `config.toml` file in the same directory, refer to [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) for configuration.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Docker
|
||||
|
||||
```shell
|
||||
docker run -d --name saveany-bot \
|
||||
-v /path/to/config.toml:/app/config.toml \
|
||||
-v /path/to/downloads:/app/downloads \
|
||||
ghcr.io/krau/saveany-bot:latest
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
Use `upgrade` or `up` command to upgrade to the latest version:
|
||||
|
||||
```bash
|
||||
./saveany-bot upgrade
|
||||
```
|
||||
|
||||
If deployed with Docker, use the following commands to update:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/krau/saveany-bot:latest
|
||||
docker restart saveany-bot
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Send (forward) files to the Bot and follow the prompts.
|
||||
|
||||
---
|
||||
|
||||
## Sponsors
|
||||
|
||||
This project is supported by [YxVM](https://yxvm.com/) and [NodeSupport](https://github.com/NodeSeekDev/NodeSupport).
|
||||
|
||||
You can consider sponsoring me if this project helps you:
|
||||
|
||||
- [Afdian](https://afdian.com/a/acherkrau)
|
||||
|
||||
## Thanks
|
||||
|
||||
- [gotd](https://github.com/gotd/td)
|
||||
- [TG-FileStreamBot](https://github.com/EverythingSuckz/TG-FileStreamBot)
|
||||
- [gotgproto](https://github.com/celestix/gotgproto)
|
||||
- All the dependencies
|
||||
@@ -12,6 +12,7 @@ func handleHelpCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
const helpText string = `
|
||||
Save Any Bot - 转存你的 Telegram 文件
|
||||
版本: %s , 提交: %s
|
||||
|
||||
命令:
|
||||
/start - 开始使用
|
||||
/help - 显示帮助
|
||||
@@ -19,12 +20,12 @@ Save Any Bot - 转存你的 Telegram 文件
|
||||
/storage - 设置默认存储位置
|
||||
/save [自定义文件名] - 保存文件
|
||||
|
||||
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
|
||||
|
||||
默认存储位置: 在静默模式下保存到的位置
|
||||
|
||||
向 Bot 发送(转发)文件, 或发送一个公开频道的消息链接以保存文件
|
||||
使用帮助: https://sabot.unv.app/usage/
|
||||
`
|
||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf(helpText, consts.Version, consts.GitCommit)), nil)
|
||||
shortHash := consts.GitCommit
|
||||
if len(shortHash) > 7 {
|
||||
shortHash = shortHash[:7]
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf(helpText, consts.Version, shortHash)), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/celestix/gotgproto/dispatcher"
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/celestix/gotgproto/functions"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/mediautil"
|
||||
@@ -23,7 +25,7 @@ func handleSaveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
logger := log.FromContext(ctx)
|
||||
args := strings.Split(string(update.EffectiveMessage.Text), " ")
|
||||
if len(args) >= 3 {
|
||||
return handleBatchSave(ctx, update, args[1], args[2])
|
||||
return handleBatchSave(ctx, update, args[1:])
|
||||
}
|
||||
replyTo := update.EffectiveMessage.ReplyToMessage
|
||||
if replyTo == nil || replyTo.Message == nil {
|
||||
@@ -60,7 +62,7 @@ func handleSaveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
func handleSilentSaveReplied(ctx *ext.Context, update *ext.Update) error {
|
||||
args := strings.Split(string(update.EffectiveMessage.Text), " ")
|
||||
if len(args) >= 3 {
|
||||
return handleBatchSave(ctx, update, args[1], args[2])
|
||||
return handleBatchSave(ctx, update, args[1:])
|
||||
}
|
||||
logger := log.FromContext(ctx)
|
||||
stor := storage.FromContext(ctx)
|
||||
@@ -92,7 +94,20 @@ func handleSilentSaveReplied(ctx *ext.Context, update *ext.Update) error {
|
||||
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, update.GetUserChat().GetID(), stor, "", file, msg.GetID())
|
||||
}
|
||||
|
||||
func handleBatchSave(ctx *ext.Context, update *ext.Update, chatArg string, msgIdRangeArg string) error {
|
||||
func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error {
|
||||
chatArg := args[0]
|
||||
msgIdRangeArg := args[1]
|
||||
var filterStr string
|
||||
var filter *regexp.Regexp
|
||||
if len(args) > 2 {
|
||||
filterStr = args[2]
|
||||
var err error
|
||||
filter, err = regexp.Compile(filterStr)
|
||||
if err != nil {
|
||||
ctx.Reply(update, ext.ReplyTextString("无效的正则表达式: "+err.Error()), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
}
|
||||
startID, endID, err := strutil.ParseIntStrRange(msgIdRangeArg, "-")
|
||||
if err != nil {
|
||||
ctx.Reply(update, ext.ReplyTextString("无效的消息ID范围: "+err.Error()), nil)
|
||||
@@ -121,7 +136,11 @@ func handleBatchSave(ctx *ext.Context, update *ext.Update, chatArg string, msgId
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
files := make([]tfile.TGFileMessage, 0, len(msgs))
|
||||
sb := strings.Builder{}
|
||||
for _, msg := range msgs {
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
media, ok := msg.GetMedia()
|
||||
if !ok {
|
||||
continue
|
||||
@@ -135,6 +154,17 @@ func handleBatchSave(ctx *ext.Context, update *ext.Update, chatArg string, msgId
|
||||
log.FromContext(ctx).Errorf("获取文件失败: %s", err)
|
||||
continue
|
||||
}
|
||||
if filter != nil {
|
||||
sb.Reset()
|
||||
sb.WriteString(msg.GetMessage())
|
||||
sb.WriteString(" ")
|
||||
fn, _ := functions.GetMediaFileNameWithId(media)
|
||||
sb.WriteString(fn)
|
||||
log.FromContext(ctx).Debugf("正在检查消息内容: %s", sb.String())
|
||||
if !filter.MatchString(sb.String()) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
|
||||
@@ -81,7 +81,29 @@ func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Up
|
||||
}
|
||||
|
||||
files = make([]tfile.TGFileMessage, 0, len(msgLinks))
|
||||
addFile := func(msg *tg.Message) {
|
||||
if msg == nil {
|
||||
logger.Warn("message is nil, skipping")
|
||||
return
|
||||
}
|
||||
media, ok := msg.GetMedia()
|
||||
if !ok {
|
||||
logger.Debugf("message %d has no media", msg.GetID())
|
||||
return
|
||||
}
|
||||
file, err := tfile.FromMediaMessage(media, msg, tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*msg)))
|
||||
if err != nil {
|
||||
logger.Errorf("failed to create file from media: %s", err)
|
||||
return
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
for _, link := range msgLinks {
|
||||
linkUrl, err := url.Parse(link)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to parse message link %s: %s", link, err)
|
||||
continue
|
||||
}
|
||||
chatId, msgId, err := tgutil.ParseMessageLink(ctx, link)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to parse message link %s: %s", link, err)
|
||||
@@ -92,17 +114,19 @@ func GetFilesFromUpdateLinkMessageWithReplyEdit(ctx *ext.Context, update *ext.Up
|
||||
logger.Errorf("failed to get message by ID: %s", err)
|
||||
continue
|
||||
}
|
||||
media, ok := msg.GetMedia()
|
||||
if !ok {
|
||||
logger.Debugf("message %d has no media", msg.GetID())
|
||||
continue
|
||||
groupID, isGroup := msg.GetGroupedID()
|
||||
if isGroup && groupID != 0 && !linkUrl.Query().Has("single") {
|
||||
gmsgs, err := tgutil.GetGroupedMessages(ctx, chatId, msg)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to get grouped messages: %s", err)
|
||||
} else {
|
||||
for _, gmsg := range gmsgs {
|
||||
addFile(gmsg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addFile(msg)
|
||||
}
|
||||
file, err := tfile.FromMediaMessage(media, msg, tfile.WithNameIfEmpty(tgutil.GenFileNameFromMessage(*msg)))
|
||||
if err != nil {
|
||||
logger.Errorf("failed to create file from media: %s", err)
|
||||
continue
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
editReplied("没有找到可保存的文件", nil)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/krau/SaveAny-Bot/client/bot"
|
||||
userclient "github.com/krau/SaveAny-Bot/client/user"
|
||||
"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/fsutil"
|
||||
@@ -46,6 +47,7 @@ func initAll(ctx context.Context) {
|
||||
fmt.Println("Failed to load config:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cache.Init()
|
||||
logger := log.FromContext(ctx)
|
||||
i18n.Init(config.Cfg.Lang)
|
||||
logger.Info(i18n.T(i18nk.Initing))
|
||||
|
||||
15
common/cache/ristretto.go
vendored
15
common/cache/ristretto.go
vendored
@@ -2,19 +2,22 @@ package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dgraph-io/ristretto/v2"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
)
|
||||
|
||||
var cache *ristretto.Cache[string, any]
|
||||
|
||||
|
||||
// TODO: maybe we should use simple ttl cache instead of ristretto...
|
||||
func init() {
|
||||
func Init() {
|
||||
if cache != nil {
|
||||
panic("cache already initialized")
|
||||
}
|
||||
c, err := ristretto.NewCache(&ristretto.Config[string, any]{
|
||||
NumCounters: 1e5,
|
||||
MaxCost: 1e6, // 1000000 / 112 ≈ 8928
|
||||
NumCounters: config.Cfg.Cache.NumCounters,
|
||||
MaxCost: config.Cfg.Cache.MaxCost,
|
||||
BufferItems: 64,
|
||||
OnReject: func(item *ristretto.Item[any]) {
|
||||
log.Warnf("Cache item rejected: key=%d, value=%v", item.Key, item.Value)
|
||||
@@ -27,7 +30,7 @@ func init() {
|
||||
}
|
||||
|
||||
func Set(key string, value any) error {
|
||||
ok := cache.Set(key, value, 0)
|
||||
ok := cache.SetWithTTL(key, value, 0, time.Duration(config.Cfg.Cache.TTL)*time.Second)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to set value in cache")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
lcstrutil "github.com/duke-git/lancet/v2/strutil"
|
||||
@@ -159,6 +160,96 @@ func GetMessagesRange(ctx *ext.Context, chatID int64, minId, maxId int) ([]*tg.M
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type MessageItem struct {
|
||||
Message *tg.Message
|
||||
Error error
|
||||
}
|
||||
|
||||
func IterMessages(ctx *ext.Context, chatID int64, minId, maxId int) (<-chan MessageItem, error) {
|
||||
total := maxId - minId + 1
|
||||
ch := make(chan MessageItem, 100)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
if !ctx.Self.Bot {
|
||||
perr := ctx.PeerStorage.GetInputPeerById(chatID)
|
||||
if perr == nil || perr.(*tg.InputPeerEmpty) != nil {
|
||||
ch <- MessageItem{
|
||||
Error: fmt.Errorf("peer not found: %d", chatID),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < total; i += 100 {
|
||||
start := minId + i
|
||||
end := min(start+100, maxId)
|
||||
msgs, err := ctx.Raw.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
|
||||
Peer: perr,
|
||||
OffsetID: start,
|
||||
AddOffset: start - end,
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
ch <- MessageItem{
|
||||
Error: fmt.Errorf("failed to get messages: %w", err),
|
||||
}
|
||||
return
|
||||
}
|
||||
var msgClass []tg.MessageClass
|
||||
switch msgsv := msgs.(type) {
|
||||
case *tg.MessagesMessages:
|
||||
msgClass = msgsv.GetMessages()
|
||||
case *tg.MessagesMessagesSlice:
|
||||
msgClass = msgsv.GetMessages()
|
||||
case *tg.MessagesChannelMessages:
|
||||
msgClass = msgsv.GetMessages()
|
||||
default:
|
||||
ch <- MessageItem{
|
||||
Error: fmt.Errorf("unsupported message type: %T", msgsv),
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, msg := range msgClass {
|
||||
msg, ok := msg.AsNotEmpty()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch msg := msg.(type) {
|
||||
case *tg.Message:
|
||||
key := fmt.Sprintf("tgmsg:%d:%d:%d", ctx.Self.ID, chatID, msg.GetID())
|
||||
cache.Set(key, msg)
|
||||
ch <- MessageItem{
|
||||
Message: msg,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < total; i += 100 {
|
||||
start := minId + i
|
||||
end := min(start+100, maxId)
|
||||
msgs, err := GetMessagesRange(ctx, chatID, start, end)
|
||||
if err != nil {
|
||||
ch <- MessageItem{
|
||||
Error: fmt.Errorf("failed to get messages: %w", err),
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, msg := range msgs {
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
ch <- MessageItem{
|
||||
Message: msg,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func GetMessageByID(ctx *ext.Context, chatID int64, msgID int) (*tg.Message, error) {
|
||||
key := fmt.Sprintf("tgmsg:%d:%d:%d", ctx.Self.ID, chatID, msgID)
|
||||
if msg, ok := cache.Get[*tg.Message](key); ok {
|
||||
@@ -181,3 +272,31 @@ func GetMessageByID(ctx *ext.Context, chatID int64, msgID int) (*tg.Message, err
|
||||
cache.Set(key, tgm)
|
||||
return tgm, nil
|
||||
}
|
||||
|
||||
func GetGroupedMessages(ctx *ext.Context, chatID int64, msg *tg.Message) ([]*tg.Message, error) {
|
||||
groupID, isGroup := msg.GetGroupedID()
|
||||
if !isGroup || groupID == 0 {
|
||||
return nil, fmt.Errorf("message %d is not grouped", msg.GetID())
|
||||
}
|
||||
msgID := msg.GetID()
|
||||
minID := msgID - 10
|
||||
maxID := msgID + 10
|
||||
if minID < 1 {
|
||||
minID = 1
|
||||
}
|
||||
msgs, err := GetMessagesRange(ctx, chatID, minID, maxID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get grouped messages: %w", err)
|
||||
}
|
||||
groupedMessages := make([]*tg.Message, 0, len(msgs))
|
||||
for _, m := range msgs {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
mgid, isGroup := m.GetGroupedID()
|
||||
if isGroup && mgid == groupID {
|
||||
groupedMessages = append(groupedMessages, m)
|
||||
}
|
||||
}
|
||||
return groupedMessages, nil
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ token = ""
|
||||
|
||||
# 初始化超时时间, 单位: 秒
|
||||
timeout = 60
|
||||
|
||||
# flood_retry = 5
|
||||
# rpc_retry = 5
|
||||
|
||||
@@ -85,18 +84,15 @@ secret_access_key = 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG'
|
||||
bucket_name = 'saveanybot'
|
||||
base_path = '/path/telegram'
|
||||
|
||||
|
||||
# 其他配置
|
||||
|
||||
# [log]
|
||||
# # 日志等级
|
||||
# level = "DEBUG"
|
||||
[[storages]]
|
||||
name = "mychannel"
|
||||
type = "telegram"
|
||||
enable = true
|
||||
chat_id = 1820371480
|
||||
|
||||
# [temp]
|
||||
# # 下载文件临时目录, 请不要在此目录下存放任何其他文件
|
||||
# base_path = "cache/"
|
||||
# # 临时文件保存时间, 单位: 秒
|
||||
# cache_ttl = 30
|
||||
|
||||
# [db]
|
||||
# path = "data/data.db" # 数据库文件路径
|
||||
|
||||
7
config/cache.go
Normal file
7
config/cache.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
type cacheConfig struct {
|
||||
TTL int64 `toml:"ttl" mapstructure:"ttl" json:"ttl"`
|
||||
NumCounters int64 `toml:"num_counters" mapstructure:"num_counters" json:"num_counters"`
|
||||
MaxCost int64 `toml:"max_cost" mapstructure:"max_cost" json:"max_cost"`
|
||||
}
|
||||
6
config/db.go
Normal file
6
config/db.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
type dbConfig struct {
|
||||
Path string `toml:"path" mapstructure:"path"`
|
||||
Session string `toml:"session" mapstructure:"session"`
|
||||
}
|
||||
22
config/hook.go
Normal file
22
config/hook.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package config
|
||||
|
||||
type hookConfig struct {
|
||||
Exec hookExecConfig `toml:"exec" mapstructure:"exec" json:"exec"`
|
||||
}
|
||||
|
||||
type hookExecConfig struct {
|
||||
// command to execute, for all task types
|
||||
TaskBeforeStart string `toml:"task_before_start" mapstructure:"task_before_start" json:"task_before_start"`
|
||||
TaskSuccess string `toml:"task_success" mapstructure:"task_success" json:"task_success"`
|
||||
TaskFail string `toml:"task_fail" mapstructure:"task_fail" json:"task_fail"`
|
||||
TaskCancel string `toml:"task_cancel" mapstructure:"task_cancel" json:"task_cancel"`
|
||||
|
||||
// TaskTypes map[string]hookExecOnTypeConfig `toml:"task_types" mapstructure:"task_types" json:"task_types"` // [TODO]
|
||||
}
|
||||
|
||||
// type hookExecOnTypeConfig struct {
|
||||
// TaskBeforeStart string `toml:"task_before_start" mapstructure:"task_before_start" json:"task_before_start"`
|
||||
// TaskSuccess string `toml:"task_success" mapstructure:"task_success" json:"task_success"`
|
||||
// TaskFail string `toml:"task_fail" mapstructure:"task_fail" json:"task_fail"`
|
||||
// TaskCancel string `toml:"task_cancel" mapstructure:"task_cancel" json:"task_cancel"`
|
||||
// }
|
||||
5
config/temp.go
Normal file
5
config/temp.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package config
|
||||
|
||||
type tempConfig struct {
|
||||
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||
}
|
||||
20
config/tg.go
Normal file
20
config/tg.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package config
|
||||
|
||||
type telegramConfig struct {
|
||||
Token string `toml:"token" mapstructure:"token"`
|
||||
AppID int `toml:"app_id" mapstructure:"app_id" json:"app_id"`
|
||||
AppHash string `toml:"app_hash" mapstructure:"app_hash" json:"app_hash"`
|
||||
Proxy tgProxyConfig `toml:"proxy" mapstructure:"proxy"`
|
||||
RpcRetry int `toml:"rpc_retry" mapstructure:"rpc_retry" json:"rpc_retry"`
|
||||
Userbot userbotConfig `toml:"userbot" mapstructure:"userbot" json:"userbot"` // [TODO]
|
||||
}
|
||||
|
||||
type userbotConfig struct {
|
||||
Enable bool `toml:"enable" mapstructure:"enable"`
|
||||
Session string `toml:"session" mapstructure:"session"`
|
||||
}
|
||||
|
||||
type tgProxyConfig struct {
|
||||
Enable bool `toml:"enable" mapstructure:"enable"`
|
||||
URL string `toml:"url" mapstructure:"url"`
|
||||
}
|
||||
@@ -22,53 +22,16 @@ type Config struct {
|
||||
Threads int `toml:"threads" mapstructure:"threads" json:"threads"`
|
||||
Stream bool `toml:"stream" mapstructure:"stream" json:"stream"`
|
||||
|
||||
Users []userConfig `toml:"users" mapstructure:"users" json:"users"`
|
||||
|
||||
Cache cacheConfig `toml:"cache" mapstructure:"cache" json:"cache"`
|
||||
Users []userConfig `toml:"users" mapstructure:"users" json:"users"`
|
||||
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
||||
Log logConfig `toml:"log" mapstructure:"log"`
|
||||
DB dbConfig `toml:"db" mapstructure:"db"`
|
||||
Telegram telegramConfig `toml:"telegram" mapstructure:"telegram"`
|
||||
Storages []storage.StorageConfig `toml:"-" mapstructure:"-" json:"storages"`
|
||||
Hook hookConfig `toml:"hook" mapstructure:"hook" json:"hook"`
|
||||
}
|
||||
|
||||
type tempConfig struct {
|
||||
BasePath string `toml:"base_path" mapstructure:"base_path" json:"base_path"`
|
||||
CacheTTL int64 `toml:"cache_ttl" mapstructure:"cache_ttl" json:"cache_ttl"`
|
||||
}
|
||||
|
||||
type logConfig struct {
|
||||
Level string `toml:"level" mapstructure:"level"`
|
||||
File string `toml:"file" mapstructure:"file"`
|
||||
BackupCount uint `toml:"backup_count" mapstructure:"backup_count" json:"backup_count"`
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
Path string `toml:"path" mapstructure:"path"`
|
||||
Session string `toml:"session" mapstructure:"session"`
|
||||
Expire int64 `toml:"expire" mapstructure:"expire"`
|
||||
}
|
||||
|
||||
type telegramConfig struct {
|
||||
Token string `toml:"token" mapstructure:"token"`
|
||||
AppID int `toml:"app_id" mapstructure:"app_id" json:"app_id"`
|
||||
AppHash string `toml:"app_hash" mapstructure:"app_hash" json:"app_hash"`
|
||||
Timeout int `toml:"timeout" mapstructure:"timeout" json:"timeout"`
|
||||
Proxy proxyConfig `toml:"proxy" mapstructure:"proxy"`
|
||||
RpcRetry int `toml:"rpc_retry" mapstructure:"rpc_retry" json:"rpc_retry"`
|
||||
Userbot userbotConfig `toml:"userbot" mapstructure:"userbot" json:"userbot"`
|
||||
}
|
||||
|
||||
type userbotConfig struct {
|
||||
Enable bool `toml:"enable" mapstructure:"enable"`
|
||||
Session string `toml:"session" mapstructure:"session"`
|
||||
}
|
||||
|
||||
type proxyConfig struct {
|
||||
Enable bool `toml:"enable" mapstructure:"enable"`
|
||||
URL string `toml:"url" mapstructure:"url"`
|
||||
}
|
||||
|
||||
var Cfg *Config
|
||||
var Cfg *Config = &Config{}
|
||||
|
||||
func (c Config) GetStorageByName(name string) storage.StorageConfig {
|
||||
for _, storage := range c.Storages {
|
||||
@@ -89,28 +52,36 @@ func Init(ctx context.Context) error {
|
||||
replacer := strings.NewReplacer(".", "_")
|
||||
viper.SetEnvKeyReplacer(replacer)
|
||||
|
||||
viper.SetDefault("lang", "zh-Hans")
|
||||
defaultConfigs := map[string]any{
|
||||
// 基础配置
|
||||
"lang": "zh-Hans",
|
||||
"workers": 3,
|
||||
"retry": 3,
|
||||
"threads": 4,
|
||||
|
||||
viper.SetDefault("workers", 3)
|
||||
viper.SetDefault("retry", 3)
|
||||
viper.SetDefault("threads", 4)
|
||||
// 缓存配置
|
||||
"cache.ttl": 86400,
|
||||
"cache.num_counters": 1e5,
|
||||
"cache.max_cost": 1e6,
|
||||
|
||||
viper.SetDefault("telegram.app_id", 1025907)
|
||||
viper.SetDefault("telegram.app_hash", "452b0359b988148995f22ff0f4229750")
|
||||
viper.SetDefault("telegram.timeout", 60)
|
||||
viper.SetDefault("telegram.flood_retry", 5)
|
||||
viper.SetDefault("telegram.rpc_retry", 5)
|
||||
viper.SetDefault("telegram.userbot.enable", false)
|
||||
viper.SetDefault("telegram.userbot.session", "data/usersession.db")
|
||||
// Telegram
|
||||
"telegram.app_id": 1025907,
|
||||
"telegram.app_hash": "452b0359b988148995f22ff0f4229750",
|
||||
"telegram.rpc_retry": 5,
|
||||
"telegram.userbot.enable": false,
|
||||
"telegram.userbot.session": "data/usersession.db",
|
||||
|
||||
viper.SetDefault("temp.base_path", "cache/")
|
||||
viper.SetDefault("temp.cache_ttl", 30)
|
||||
// 临时目录
|
||||
"temp.base_path": "cache/",
|
||||
|
||||
viper.SetDefault("log.level", "INFO")
|
||||
// 数据库
|
||||
"db.path": "data/saveany.db",
|
||||
"db.session": "data/session.db",
|
||||
}
|
||||
|
||||
viper.SetDefault("db.path", "data/saveany.db")
|
||||
viper.SetDefault("db.session", "data/session.db")
|
||||
viper.SetDefault("db.expire", 86400*5)
|
||||
for key, value := range defaultConfigs {
|
||||
viper.SetDefault(key, value)
|
||||
}
|
||||
|
||||
if err := viper.SafeWriteConfigAs("config.toml"); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileAlreadyExistsError); !ok {
|
||||
@@ -123,8 +94,6 @@ func Init(ctx context.Context) error {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
Cfg = &Config{}
|
||||
|
||||
if err := viper.Unmarshal(Cfg); err != nil {
|
||||
fmt.Println("Error unmarshalling config file, ", err)
|
||||
os.Exit(1)
|
||||
@@ -171,7 +140,6 @@ func Init(ctx context.Context) error {
|
||||
userStorages[user.ID] = user.Storages
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||
"github.com/krau/SaveAny-Bot/common/utils/ioutil"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/key"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
@@ -104,7 +104,7 @@ func (t *Task) processElement(ctx context.Context, elem TaskElement) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file stat: %w", err)
|
||||
}
|
||||
vctx := context.WithValue(ctx, key.ContextKeyContentLength, fileStat.Size())
|
||||
vctx := context.WithValue(ctx, ctxkey.ContentLength, fileStat.Size())
|
||||
err = retry.Retry(func() error {
|
||||
var file *os.File
|
||||
file, err = os.Open(elem.localPath)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/krau/SaveAny-Bot/common/tdler"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
"github.com/rs/xid"
|
||||
@@ -35,6 +36,10 @@ type Task struct {
|
||||
failed map[string]error // errors for each element
|
||||
}
|
||||
|
||||
func (t *Task) Type() tasktype.TaskType {
|
||||
return tasktype.TaskTypeTgfiles
|
||||
}
|
||||
|
||||
func NewTaskElement(
|
||||
stor storage.Storage,
|
||||
path string,
|
||||
|
||||
28
core/core.go
28
core/core.go
@@ -2,32 +2,54 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||
"github.com/krau/SaveAny-Bot/pkg/queue"
|
||||
)
|
||||
|
||||
var queueInstance *queue.TaskQueue[Exectable]
|
||||
|
||||
type Exectable interface {
|
||||
Type() tasktype.TaskType
|
||||
TaskID() string
|
||||
Execute(ctx context.Context) error
|
||||
}
|
||||
|
||||
func worker(ctx context.Context, qe *queue.TaskQueue[Exectable], semaphore chan struct{}) {
|
||||
logger := log.FromContext(ctx)
|
||||
execHooks := config.Cfg.Hook.Exec
|
||||
for {
|
||||
semaphore <- struct{}{}
|
||||
qtask, err := qe.Get()
|
||||
if err != nil {
|
||||
logger.Error("Failed to get task from queue:", err)
|
||||
break // queue closed and empty
|
||||
}
|
||||
log.FromContext(ctx).Infof("Processing task: %s", qtask.ID)
|
||||
task := qtask.Data
|
||||
logger.Infof("Processing task: %s", task.TaskID())
|
||||
if err := ExecCommandString(qtask.Context(), execHooks.TaskBeforeStart); err != nil {
|
||||
logger.Errorf("Failed to execute before start hook for task %s: %v", task.TaskID(), err)
|
||||
}
|
||||
if err := task.Execute(qtask.Context()); err != nil {
|
||||
log.FromContext(ctx).Errorf("Failed to execute task %s: %v", qtask.ID, err)
|
||||
if errors.Is(err, context.Canceled) {
|
||||
logger.Infof("Task %s was canceled", task.TaskID())
|
||||
if err := ExecCommandString(ctx, execHooks.TaskCancel); err != nil {
|
||||
logger.Errorf("Failed to execute cancel hook for task %s: %v", task.TaskID(), err)
|
||||
}
|
||||
} else {
|
||||
logger.Errorf("Failed to execute task %s: %v", task.TaskID(), err)
|
||||
if err := ExecCommandString(ctx, execHooks.TaskFail); err != nil {
|
||||
logger.Errorf("Failed to execute fail hook for task %s: %v", task.TaskID(), err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.FromContext(ctx).Infof("Task %s completed successfully", qtask.ID)
|
||||
logger.Infof("Task %s completed successfully", task.TaskID())
|
||||
if err := ExecCommandString(ctx, execHooks.TaskSuccess); err != nil {
|
||||
logger.Errorf("Failed to execute success hook for task %s: %v", task.TaskID(), err)
|
||||
}
|
||||
}
|
||||
qe.Done(qtask.ID)
|
||||
<-semaphore
|
||||
|
||||
23
core/hookutil.go
Normal file
23
core/hookutil.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func ExecCommandString(ctx context.Context, cmd string) error {
|
||||
if cmd == "" {
|
||||
return nil
|
||||
}
|
||||
var execCmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
execCmd = exec.CommandContext(ctx, "cmd.exe", "/C", cmd)
|
||||
} else {
|
||||
execCmd = exec.CommandContext(ctx, "sh", "-c", cmd)
|
||||
}
|
||||
execCmd.Stdout = os.Stdout
|
||||
execCmd.Stderr = os.Stderr
|
||||
return execCmd.Run()
|
||||
}
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"github.com/krau/SaveAny-Bot/common/tdler"
|
||||
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/key"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||
)
|
||||
|
||||
func (t *TGFileTask) Execute(ctx context.Context) error {
|
||||
func (t *Task) Execute(ctx context.Context) error {
|
||||
logger := log.FromContext(ctx).WithPrefix(fmt.Sprintf("file[%s]", t.File.Name()))
|
||||
t.Progress.OnStart(ctx, t)
|
||||
if t.stream {
|
||||
@@ -52,7 +52,7 @@ func (t *TGFileTask) Execute(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file stat: %w", err)
|
||||
}
|
||||
vctx := context.WithValue(ctx, key.ContextKeyContentLength, fileStat.Size())
|
||||
vctx := context.WithValue(ctx, ctxkey.ContentLength, fileStat.Size())
|
||||
for i := range config.Cfg.Retry + 1 {
|
||||
if err = vctx.Err(); err != nil {
|
||||
return fmt.Errorf("context canceled while saving file: %w", err)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func executeStream(ctx context.Context, task *TGFileTask) error {
|
||||
func executeStream(ctx context.Context, task *Task) error {
|
||||
logger := log.FromContext(ctx).WithPrefix(fmt.Sprintf("file[%s]", task.File.Name()))
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
@@ -8,22 +8,22 @@ type TaskInfo interface {
|
||||
StorageName() string
|
||||
}
|
||||
|
||||
func (t *TGFileTask) TaskID() string {
|
||||
func (t *Task) TaskID() string {
|
||||
return t.ID
|
||||
}
|
||||
|
||||
func (t *TGFileTask) FileName() string {
|
||||
func (t *Task) FileName() string {
|
||||
return t.File.Name()
|
||||
}
|
||||
|
||||
func (t *TGFileTask) FileSize() int64 {
|
||||
func (t *Task) FileSize() int64 {
|
||||
return t.File.Size()
|
||||
}
|
||||
|
||||
func (t *TGFileTask) StoragePath() string {
|
||||
func (t *Task) StoragePath() string {
|
||||
return t.Path
|
||||
}
|
||||
|
||||
func (t *TGFileTask) StorageName() string {
|
||||
func (t *Task) StorageName() string {
|
||||
return t.Storage.Name()
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
|
||||
"github.com/krau/SaveAny-Bot/common/tdler"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
)
|
||||
|
||||
type TGFileTask struct {
|
||||
type Task struct {
|
||||
ID string
|
||||
Ctx context.Context
|
||||
File tfile.TGFile
|
||||
@@ -23,6 +24,10 @@ type TGFileTask struct {
|
||||
localPath string
|
||||
}
|
||||
|
||||
func (t *Task) Type() tasktype.TaskType {
|
||||
return tasktype.TaskTypeTgfiles
|
||||
}
|
||||
|
||||
func NewTGFileTask(
|
||||
id string,
|
||||
ctx context.Context,
|
||||
@@ -31,14 +36,14 @@ func NewTGFileTask(
|
||||
stor storage.Storage,
|
||||
path string,
|
||||
progress ProgressTracker,
|
||||
) (*TGFileTask, error) {
|
||||
) (*Task, error) {
|
||||
_, ok := stor.(storage.StorageCannotStream)
|
||||
if !config.Cfg.Stream || ok {
|
||||
cachePath, err := filepath.Abs(filepath.Join(config.Cfg.Temp.BasePath, fmt.Sprintf("%s_%s", id, file.Name())))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path for cache: %w", err)
|
||||
}
|
||||
tftask := &TGFileTask{
|
||||
tftask := &Task{
|
||||
ID: id,
|
||||
Ctx: ctx,
|
||||
client: client,
|
||||
@@ -50,7 +55,7 @@ func NewTGFileTask(
|
||||
}
|
||||
return tftask, nil
|
||||
}
|
||||
tfileTask := &TGFileTask{
|
||||
tfileTask := &Task{
|
||||
ID: id,
|
||||
Ctx: ctx,
|
||||
client: client,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||
"github.com/krau/SaveAny-Bot/pkg/telegraph"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
)
|
||||
@@ -23,6 +24,10 @@ type Task struct {
|
||||
downloaded atomic.Int64
|
||||
}
|
||||
|
||||
func (t *Task) Type() tasktype.TaskType {
|
||||
return tasktype.TaskTypeTphpics
|
||||
}
|
||||
|
||||
func NewTask(
|
||||
id string,
|
||||
ctx context.Context,
|
||||
|
||||
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
public/
|
||||
5
docs/archetypes/default.md
Normal file
5
docs/archetypes/default.md
Normal file
@@ -0,0 +1,5 @@
|
||||
+++
|
||||
date = '{{ .Date }}'
|
||||
draft = true
|
||||
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||
+++
|
||||
1
docs/assets/_variables.scss
Normal file
1
docs/assets/_variables.scss
Normal file
@@ -0,0 +1 @@
|
||||
$font-size-base: 18px;
|
||||
31
docs/content/en/_index.md
Normal file
31
docs/content/en/_index.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: Introduction
|
||||
---
|
||||
|
||||
# Save Any Bot
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Save Any Bot is a tool that allows you to save files from Telegram to various storage backends.
|
||||
|
||||
## Features
|
||||
|
||||
- Supports documents/videos/images/stickers... and even Telegraph
|
||||
- Breaks restrictions on saving files
|
||||
- Batch download
|
||||
- Streaming
|
||||
- Multi-user
|
||||
- Automatic organization based on storage rules
|
||||
- Supports multiple storage backends:
|
||||
- Alist
|
||||
- Minio (S3 compatible)
|
||||
- WebDAV
|
||||
- Telegram (re-upload to specified chat)
|
||||
- Local disk
|
||||
|
||||
## [Contributors](https://github.com/krau/SaveAny-Bot/graphs/contributors)
|
||||
|
||||

|
||||
14
docs/content/en/contribute/_index.md
Normal file
14
docs/content/en/contribute/_index.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: "Contributing"
|
||||
weight: 20
|
||||
---
|
||||
|
||||
# Contributing
|
||||
|
||||
## Contributing New Storage Backend
|
||||
|
||||
1. Fork this repository and clone it to your local machine.
|
||||
2. Add the new storage backend type in `pkg/enums/storage/storages.go` and run code generation.
|
||||
3. Define the storage backend configuration in the `config/storage` directory and add it to `config/storage/factory.go`.
|
||||
4. Create a new package in the `storage` directory, implement the storage backend, and import it in `storage/storage.go`.
|
||||
5. Update the documentation to include configuration details for the new storage backend.
|
||||
4
docs/content/en/deployment/_index.md
Normal file
4
docs/content/en/deployment/_index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Deployment Guide"
|
||||
weight: 5
|
||||
---
|
||||
141
docs/content/en/deployment/configuration/_index.md
Normal file
141
docs/content/en/deployment/configuration/_index.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: "Configuration Guide"
|
||||
---
|
||||
|
||||
# Configuration Guide
|
||||
|
||||
SaveAnyBot uses the toml format for its configuration files. You can learn more about toml syntax on the [TOML official website](https://toml.io/).
|
||||
|
||||
SaveAnyBot needs to read a `config.toml` file in the working directory as its configuration file. If this file is missing, a default file will be created, and the bot will attempt to load configuration from environment variables.
|
||||
|
||||
Here is an example of a minimal configuration file:
|
||||
|
||||
```toml
|
||||
[telegram]
|
||||
token = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
[[users]]
|
||||
# telegram user id
|
||||
id = 777000
|
||||
blacklist = true
|
||||
|
||||
[[storages]]
|
||||
name = "Local Storage"
|
||||
type = "local"
|
||||
enable = true
|
||||
base_path = "./downloads"
|
||||
```
|
||||
|
||||
## Detailed Configuration
|
||||
|
||||
### Global Configuration
|
||||
|
||||
- `stream`: Whether to enable Stream mode, default is `false`. When enabled, the Bot will stream files directly to storage endpoints (if supported), without downloading them locally.
|
||||
{{< hint warning >}}
|
||||
Stream mode is very useful for deployment environments with limited disk space, but it also has some drawbacks:
|
||||
<br />
|
||||
<ul>
|
||||
<li>Cannot use multi-threading to download files from Telegram, resulting in slower speeds.</li>
|
||||
<li>Higher task failure rate when the network is unstable.</li>
|
||||
<li>Cannot process files in the middle layer, such as automatic file type identification.</li>
|
||||
<li>Not supported by all storage endpoints; unsupported endpoints may downgrade to normal mode or fail to upload.</li>
|
||||
</ul>
|
||||
{{< /hint >}}
|
||||
- `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.
|
||||
- `retry`: Number of retries when a task fails, default is 3.
|
||||
|
||||
### Telegram Configuration
|
||||
|
||||
- `token`: Your Telegram Bot Token, which can be obtained by creating a Bot through [BotFather](https://t.me/botfather).
|
||||
- `app_id`, `app_hash`: Telegram API ID & Hash, obtained by creating an application at [Telegram API](https://my.telegram.org/apps). Default values will be used if not provided.
|
||||
- `flood_retry`: Number of retries for flood control, default is 5.
|
||||
- `rpc_retry`: Number of retries for RPC requests, default is 5.
|
||||
- `proxy`: Proxy configuration, optional.
|
||||
- `enable`: Whether to enable the proxy.
|
||||
- `url`: Proxy address, only supports `socks5://`
|
||||
|
||||
```toml
|
||||
[telegram]
|
||||
token = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
app_id = 1025907
|
||||
app_hash = "452b0359b988148995f22ff0f4229750"
|
||||
flood_retry = 5
|
||||
rpc_retry = 5
|
||||
[telegram.proxy]
|
||||
enable = false
|
||||
url = "socks5://127.0.0.1:7890"
|
||||
```
|
||||
|
||||
### Storage Endpoints List
|
||||
|
||||
The storage endpoints list is used to define the storage locations supported by the Bot. Each storage endpoint needs to specify a name, type, and related configuration, using the double bracket syntax `[[storages]]`.
|
||||
|
||||
Each storage endpoint requires at least the following fields:
|
||||
|
||||
- `name`: Storage endpoint name, used for identification in the Bot, must be unique.
|
||||
- `enable`: Whether to enable this storage endpoint, default is `true`.
|
||||
- `type`: Storage endpoint type, currently supports the following types:
|
||||
- `local`: Local disk
|
||||
- `alist`: Alist
|
||||
- `webdav`: WebDAV
|
||||
- `minio`: MinIO (compatible with S3 API)
|
||||
- `telegram`: Upload to Telegram
|
||||
|
||||
Example, this is a configuration that includes local storage and webdav storage:
|
||||
|
||||
```toml
|
||||
[[storages]]
|
||||
name = "Local Storage"
|
||||
type = "local"
|
||||
enable = true
|
||||
# Custom configuration for local type storage
|
||||
base_path = "./downloads"
|
||||
|
||||
[[storages]]
|
||||
name = "WebDAV"
|
||||
type = "webdav"
|
||||
enable = true
|
||||
# Custom configuration for webdav type storage
|
||||
url = "https://example.com/webdav"
|
||||
base_path = "/path/to/webdav"
|
||||
username = "your_username"
|
||||
password = "your_password"
|
||||
```
|
||||
|
||||
For custom configuration items for all storage endpoints, see [Storage Configuration](./storages)
|
||||
|
||||
### User List
|
||||
|
||||
The user list is used to define access control for storage endpoints. Each user needs to specify a Telegram User ID, defined using the double bracket syntax `[[users]]`.
|
||||
|
||||
- `id`: The user's Telegram User ID
|
||||
- `storages`: Filtered list of storage endpoints, defined by storage endpoint names, default is whitelist mode (i.e., only allows access to storage endpoints in the list)
|
||||
- `blacklist`: Whether to enable blacklist mode, default is `false`. If blacklist mode is enabled, the user is allowed to access only storage endpoints that are **not** in the list.
|
||||
|
||||
Example, this is a configuration containing three users: user `123123` can only access local storage, user `456456` can only access storage other than WebDAV, and user `789789` has blacklist mode enabled but no storage endpoints specified, so they can access all storage:
|
||||
|
||||
```toml
|
||||
[[users]]
|
||||
id = 123123
|
||||
storages = ["Local Storage"]
|
||||
|
||||
[[users]]
|
||||
id = 456456
|
||||
storages = ["WebDAV"]
|
||||
blacklist = true
|
||||
|
||||
[[users]]
|
||||
id = 789789
|
||||
storages = []
|
||||
blacklist = true
|
||||
```
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
```toml
|
||||
no_clean_cache = false # Whether not to clear the cache folder when exiting
|
||||
# Temporary download folder configuration
|
||||
[temp]
|
||||
base_path = "./cache"
|
||||
```
|
||||
65
docs/content/en/deployment/configuration/storages.md
Normal file
65
docs/content/en/deployment/configuration/storages.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: "Storage Configuration"
|
||||
---
|
||||
|
||||
# Storage Configuration
|
||||
|
||||
Please first read the [Configuration Guide](../) to understand the basic format of the configuration file.
|
||||
|
||||
## Alist
|
||||
|
||||
`type=alist`
|
||||
|
||||
Stream mode is not supported.
|
||||
|
||||
```toml
|
||||
url = "https://alist.example.com" # URL of Alist
|
||||
username = "your_username" # Username for Alist
|
||||
password = "your_password" # Password for Alist
|
||||
base_path = "/path/saveanybot" # Base path in Alist, all files will be stored under this path
|
||||
token_exp = 3600 # Auto-refresh time for Alist access token, in seconds
|
||||
token = "your_token"
|
||||
# Access token for Alist, optional, if not set, username and password will be used for authentication.
|
||||
# When using token authentication, the token cannot be automatically refreshed
|
||||
```
|
||||
|
||||
## Local Disk
|
||||
|
||||
`type=local`
|
||||
|
||||
```toml
|
||||
base_path = "./downloads" # Base path for local storage, all files will be stored under this path
|
||||
```
|
||||
|
||||
## WebDAV
|
||||
`type=webdav`
|
||||
|
||||
```toml
|
||||
url = "https://webdav.example.com" # URL of WebDAV
|
||||
username = "your_username" # Username for WebDAV
|
||||
password = "your_password" # Password for WebDAV
|
||||
base_path = "/path/to/webdav" # Base path in WebDAV, all files will be stored under this path
|
||||
```
|
||||
|
||||
## MinIO (S3)
|
||||
|
||||
`type=minio`
|
||||
|
||||
```toml
|
||||
endpoint = "minio.example.com" # Endpoint for MinIO or S3
|
||||
access_key_id = "your_access_key_id" # Access key ID for MinIO or S3
|
||||
secret_access_key = "your_secret_access_key" # Secret access key for MinIO or S3
|
||||
bucket_name = "your_bucket_name" # Bucket name for MinIO or S3
|
||||
use_ssl = true # Whether to use SSL, default is true
|
||||
base_path = "/path/to/minio" # Base path in MinIO, all files will be stored under this path
|
||||
```
|
||||
|
||||
## Telegram
|
||||
|
||||
`type=telegram`
|
||||
|
||||
Stream mode is not supported.
|
||||
|
||||
```toml
|
||||
chat_id = "123456789" # Telegram chat ID, the Bot will send files to this chat
|
||||
```
|
||||
145
docs/content/en/deployment/installation.md
Normal file
145
docs/content/en/deployment/installation.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
title: "Installation and Updates"
|
||||
---
|
||||
|
||||
# Installation and Updates
|
||||
|
||||
## Deploy from Pre-compiled Files
|
||||
|
||||
Download the binary file for your platform from the [Release](https://github.com/krau/SaveAny-Bot/releases) page.
|
||||
|
||||
Create a `config.toml` file in the extracted directory, refer to the [Configuration Guide](../configuration) to edit the configuration file.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
chmod +x saveany-bot
|
||||
./saveany-bot
|
||||
```
|
||||
|
||||
### Process Monitoring
|
||||
|
||||
{{< tabs "daemon" >}}
|
||||
{{< tab "systemd (Regular Linux)" >}}
|
||||
|
||||
Create a file <code>/etc/systemd/system/saveany-bot.service</code> and write the following content:
|
||||
|
||||
{{< codeblock >}}
|
||||
[Unit]
|
||||
Description=SaveAnyBot
|
||||
After=systemd-user-sessions.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/yourpath/
|
||||
ExecStart=/yourpath/saveany-bot
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
{{< /codeblock >}}
|
||||
|
||||
Enable startup on boot and start the service:
|
||||
|
||||
{{< codeblock >}}
|
||||
systemctl enable --now saveany-bot
|
||||
{{< /codeblock >}}
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab "procd (OpenWrt)" >}}
|
||||
|
||||
<h4>Add Boot Autostart Service</h4>
|
||||
|
||||
Create a file <code>/etc/init.d/saveanybot</code>, refer to <a href="https://github.com/krau/SaveAny-Bot/blob/main/docs/confs/wrt_init" target="_blank">wrt_init</a> and modify as needed:
|
||||
|
||||
{{< codeblock >}}
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
#This is the OpenWRT init.d script for SaveAnyBot
|
||||
|
||||
START=99
|
||||
STOP=10
|
||||
description="SaveAnyBot"
|
||||
|
||||
WORKING_DIR="/mnt/mmc1-1/SaveAnyBot"
|
||||
EXEC_PATH="$WORKING_DIR/saveany-bot"
|
||||
start() {
|
||||
echo "Starting SaveAnyBot..."
|
||||
cd $WORKING_DIR
|
||||
$EXEC_PATH &
|
||||
}
|
||||
stop() {
|
||||
echo "Stopping SaveAnyBot..."
|
||||
killall saveany-bot
|
||||
}
|
||||
reload() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
{{< /codeblock >}}
|
||||
|
||||
Set permissions:
|
||||
|
||||
{{< codeblock >}}
|
||||
chmod +x /etc/init.d/saveanybot
|
||||
{{< /codeblock >}}
|
||||
|
||||
Then copy the file to <code>/etc/rc.d</code> and rename it to <code>S99saveanybot</code>, also set permissions:
|
||||
|
||||
{{< codeblock >}}
|
||||
chmod +x /etc/rc.d/S99saveanybot
|
||||
{{< /codeblock >}}
|
||||
|
||||
<h4>Add Shortcut Commands</h4>
|
||||
|
||||
Create a file <code>/usr/bin/sabot</code>, refer to <a href="https://github.com/krau/SaveAny-Bot/blob/main/docs/confs/wrt_bin" target="_blank">wrt_bin</a> and modify as needed. Note that the file encoding here only supports ANSI 936.
|
||||
|
||||
Then set permissions:
|
||||
|
||||
{{< codeblock >}}
|
||||
chmod +x /usr/bin/sabot
|
||||
{{< /codeblock >}}
|
||||
|
||||
Usage: <code>sudo sabot start|stop|restart|status|enable|disable</code>
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
|
||||
## Deploy Using Docker
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Download the [docker-compose.yml](https://github.com/krau/SaveAny-Bot/blob/main/docker-compose.yml) file, create a new `config.toml` file in the same directory, refer to [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) to edit the configuration file.
|
||||
|
||||
Start:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```shell
|
||||
docker run -d --name saveany-bot \
|
||||
-v /path/to/config.toml:/app/config.toml \
|
||||
-v /path/to/downloads:/app/downloads \
|
||||
ghcr.io/krau/saveany-bot:latest
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
Use `upgrade` or `up` to upgrade to the latest version
|
||||
|
||||
```bash
|
||||
./saveany-bot upgrade
|
||||
```
|
||||
|
||||
If you deployed with Docker, use the following commands to update:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/krau/saveany-bot:latest
|
||||
docker restart saveany-bot
|
||||
```
|
||||
18
docs/content/en/help/_index.md
Normal file
18
docs/content/en/help/_index.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: "Frequently Asked Questions"
|
||||
weight: 15
|
||||
---
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
## Upload to AList shows success but actually fails
|
||||
|
||||
Adjust the upload chunk size in the AList management page, and deploy AList in a more stable network environment to reduce the occurrence of this issue.
|
||||
|
||||
## Bot indicates successful download but files don't show up in AList
|
||||
|
||||
AList caches directory structures. Refer to the <a href="https://alist.nn.ci/guide/drivers/common.html#cache-expiration" target="_blank">documentation</a> to adjust cache expiration time.
|
||||
|
||||
## Docker deployment still can't connect to Telegram despite proxy configuration (client initialization timeout)
|
||||
|
||||
Docker cannot directly access the host network. If you're not familiar with its usage, please set the container to host mode.
|
||||
65
docs/content/en/usage/_index.md
Normal file
65
docs/content/en/usage/_index.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: "Usage"
|
||||
weight: 10
|
||||
---
|
||||
|
||||
# Usage
|
||||
|
||||
## File Transfer
|
||||
|
||||
The bot accepts two types of messages: files and links.
|
||||
|
||||
Supported links:
|
||||
|
||||
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.**
|
||||
2. Telegra.ph article links, the bot will download all images within.
|
||||
|
||||
## Silent Mode
|
||||
|
||||
Use the `/silent` command to toggle silent mode.
|
||||
|
||||
By default, silent mode is off, and the bot will ask you for the save location of each file.
|
||||
|
||||
When silent mode is enabled, the bot will save files directly to the default location without confirmation.
|
||||
|
||||
Before enabling silent mode, you need to set the default save location using the `/storage` command.
|
||||
|
||||
|
||||
## Storage Rules
|
||||
|
||||
Allows you to set some redirection rules for the bot when uploading files to storage, for automatic organization of saved files.
|
||||
|
||||
See: <a href="https://github.com/krau/SaveAny-Bot/issues/28" target="_blank">#28</a>
|
||||
|
||||
Currently supported rule types:
|
||||
|
||||
1. FILENAME-REGEX
|
||||
2. MESSAGE-REGEX
|
||||
|
||||
Basic syntax for adding rules:
|
||||
|
||||
"Rule Type Rule Content Storage Name 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:
|
||||
|
||||
```
|
||||
/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.
|
||||
|
||||
Rule descriptions:
|
||||
|
||||
### FILENAME-REGEX
|
||||
|
||||
Matches based on filename regex. The rule content must be a valid regular expression, such as:
|
||||
|
||||
```
|
||||
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).
|
||||
|
||||
### MESSAGE-REGEX
|
||||
|
||||
Similar to the above, but matches based on the text content of the message itself.
|
||||
31
docs/content/zh/_index.md
Normal file
31
docs/content/zh/_index.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: 介绍
|
||||
---
|
||||
|
||||
# Save Any Bot
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
把 Telegram 上的文件转存到多种存储端.
|
||||
|
||||
## 特性
|
||||
|
||||
- 支持文档/视频/图片/贴纸... 甚至还有 Telegraph
|
||||
- 破解禁止保存的文件
|
||||
- 批量下载
|
||||
- 流式传输
|
||||
- 多用户
|
||||
- 基于存储规则的自动整理
|
||||
- 支持多种存储端:
|
||||
- Alist
|
||||
- Minio (S3 兼容)
|
||||
- WebDAV
|
||||
- Telegram (重传回指定聊天)
|
||||
- 本地磁盘
|
||||
|
||||
## [贡献者](https://github.com/krau/SaveAny-Bot/graphs/contributors)
|
||||
|
||||

|
||||
14
docs/content/zh/contribute/_index.md
Normal file
14
docs/content/zh/contribute/_index.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: "参与开发"
|
||||
weight: 20
|
||||
---
|
||||
|
||||
# 参与开发
|
||||
|
||||
## 贡献新存储端
|
||||
|
||||
1. Fork 本项目, 克隆到本地
|
||||
2. 在 `pkg/enums/storage/storages.go` 中添加新的存储端类型, 并运行代码生成
|
||||
3. 在 `config/storage` 目录下定义存储端配置, 并添加到 `config/storage/factory.go` 中
|
||||
4. 在 `storage` 目录下新建一个包, 编写存储端实现, 然后在 `storage/storage.go` 中导入并添加它
|
||||
5. 更新文档, 添加配置说明
|
||||
4
docs/content/zh/deployment/_index.md
Normal file
4
docs/content/zh/deployment/_index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "部署指南"
|
||||
weight: 5
|
||||
---
|
||||
162
docs/content/zh/deployment/configuration/_index.md
Normal file
162
docs/content/zh/deployment/configuration/_index.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: "配置说明"
|
||||
---
|
||||
|
||||
# 配置说明
|
||||
|
||||
SaveAnyBot 的配置文件使用 toml 格式, 你可以在 [TOML 官方网站](https://toml.io/) 上了解更多关于 toml 的语法.
|
||||
|
||||
SaveAnyBot 需要读取工作目录下的 `config.toml` 文件作为配置文件, 若缺少该文件则会创建默认文件, 并尝试从环境变量中加载配置.
|
||||
|
||||
以下是一个最简的配置文件示例:
|
||||
|
||||
```toml
|
||||
[telegram]
|
||||
token = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
[[users]]
|
||||
# telegram user id
|
||||
id = 777000
|
||||
blacklist = true
|
||||
|
||||
[[storages]]
|
||||
name = "本机存储"
|
||||
type = "local"
|
||||
enable = true
|
||||
base_path = "./downloads"
|
||||
```
|
||||
|
||||
## 详细配置
|
||||
|
||||
### 全局配置
|
||||
|
||||
- `stream`: 是否启用 Stream 模式, 默认为 `false`. 启用后 Bot 将直接将文件流式传输到存储端(若存储端支持), 不需要下载到本地
|
||||
{{< hint warning >}}
|
||||
Stream 模式对于磁盘空间有限的部署环境十分有用, 但也有一些弊端:
|
||||
<br />
|
||||
<ul>
|
||||
<li>无法使用多线程从 Telegram 下载文件, 速度较慢.</li>
|
||||
<li>网络不稳定时, 任务失败率高.</li>
|
||||
<li>无法在中间层对文件进行处理, 例如自动文件类型识别.</li>
|
||||
<li>并非支持所有存储端, 不支持的存储端可能会降级为普通模式或无法上传.</li>
|
||||
</ul>
|
||||
{{< /hint >}}
|
||||
- `workers`: 同时处理任务数量, 默认为 3
|
||||
- `threads`: 下载文件时使用的线程数, 默认为 4. 仅在未启用 Stream 模式时生效.
|
||||
- `retry`: 任务失败时的重试次数, 默认为 3.
|
||||
|
||||
### Telegram 配置
|
||||
|
||||
- `token`: 你的 Telegram Bot Token, 可以通过 [BotFather](https://t.me/botfather) 创建 Bot 并获取 Token.
|
||||
- `app_id`, `app_hash`: Telegram API ID & Hash, 在 [Telegram API](https://my.telegram.org/apps) 创建应用获取, 若不提供则使用默认值.
|
||||
- `flood_retry`: Flood 控制重试次数, 默认为 5.
|
||||
- `rpc_retry`: RPC 请求重试次数, 默认为 5.
|
||||
- `proxy`: 代理配置, 可选项.
|
||||
- `enable`: 是否启用代理.
|
||||
- `url`: 代理地址, 只支持 `socks5://`
|
||||
|
||||
```toml
|
||||
[telegram]
|
||||
token = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
app_id = 1025907
|
||||
app_hash = "452b0359b988148995f22ff0f4229750"
|
||||
flood_retry = 5
|
||||
rpc_retry = 5
|
||||
[telegram.proxy]
|
||||
enable = false
|
||||
url = "socks5://127.0.0.1:7890"
|
||||
```
|
||||
|
||||
### 存储端列表
|
||||
|
||||
存储端列表用于定义 Bot 支持的存储位置, 每个存储端需要指定名称、类型和相关配置, 使用双中括号语法 `[[storages]]` 定义.
|
||||
|
||||
每一个存储端至少需要以下字段:
|
||||
|
||||
- `name`: 存储端名称, 用于在 Bot 中识别, 需要唯一
|
||||
- `enable`: 是否启用该存储端, 默认为 `true`
|
||||
- `type`: 存储端类型, 目前支持以下类型:
|
||||
- `local`: 本地磁盘
|
||||
- `alist`: Alist
|
||||
- `webdav`: WebDAV
|
||||
- `minio`: MinIO (兼容 S3 API)
|
||||
- `telegram`: 上传到 Telegram
|
||||
|
||||
示例, 这是一个包含本地存储和 webdav 存储的配置:
|
||||
|
||||
```toml
|
||||
[[storages]]
|
||||
name = "本地存储"
|
||||
type = "local"
|
||||
enable = true
|
||||
# 以下是 local 类型存储的自定义配置
|
||||
base_path = "./downloads"
|
||||
|
||||
[[storages]]
|
||||
name = "WebDAV"
|
||||
type = "webdav"
|
||||
enable = true
|
||||
# 以下是 webdav 类型存储的自定义配置
|
||||
url = "https://example.com/webdav"
|
||||
base_path = "/path/to/webdav"
|
||||
username = "your_username"
|
||||
password = "your_password"
|
||||
```
|
||||
|
||||
所有存储端的自定义配置项可查看 [存储端配置](./storages)
|
||||
|
||||
### 用户列表
|
||||
|
||||
用户列表用于定义对存储端的访问控制, 每个用户需要指定 Telegram 上的用户 ID, 使用双中括号语法 `[[users]]` 定义.
|
||||
|
||||
- `id`: 用户的 Telegram User ID
|
||||
- `storages`: 过滤的存储端列表, 使用存储端名称定义, 默认为白名单模式 (即只允许访问列表中的存储端)
|
||||
- `blacklist`: 是否启用黑名单模式, 默认为 `false`. 若启用黑名单模式, 则仅允许访问**没有**在列表中的存储端.
|
||||
|
||||
示例, 这是一个包含三个用户的配置, 用户 `123123` 只能访问本地存储, 用户 `456456` 只能访问除 WebDAV 以外的存储, 用户 `789789` 启用黑名单模式但没有指定存储端, 因此可以访问所有存储:
|
||||
|
||||
```toml
|
||||
[[users]]
|
||||
id = 123123
|
||||
storages = ["本地存储"]
|
||||
|
||||
[[users]]
|
||||
id = 456456
|
||||
storages = ["WebDAV"]
|
||||
blacklist = true
|
||||
|
||||
[[users]]
|
||||
id = 789789
|
||||
storages = []
|
||||
blacklist = true
|
||||
```
|
||||
|
||||
### 事件触发
|
||||
|
||||
事件触发提供了在 Bot 处理任务时根据任务状态执行自定义操作的能力, 目前仅支持任意命令执行. 使用 `[hook.exec]` 配置.
|
||||
|
||||
目前具有以下几种事件类型:
|
||||
|
||||
- `task_before_start`: 任务即将开始前
|
||||
- `task_success`: 任务成功完成后
|
||||
- `task_fail`: 任务失败后
|
||||
- `task_cancel`: 任务被取消后
|
||||
|
||||
提供的配置值需要为完整的命令行命令, Bot 会在事件发生时执行该命令. 示例:
|
||||
|
||||
```toml
|
||||
[hook.exec]
|
||||
task_before_start = "echo '任务即将开始'"
|
||||
task_success = "bash /path/to/success_script.sh"
|
||||
task_fail = "curl -X POST https://example.com/api/notify -d '任务失败'"
|
||||
task_cancel = "bash /path/to/cancel_script.sh"
|
||||
```
|
||||
|
||||
### 杂项
|
||||
|
||||
```toml
|
||||
no_clean_cache = false # 是否在退出时不清空缓存文件夹
|
||||
# 临时下载文件夹配置
|
||||
[temp]
|
||||
base_path = "./cache"
|
||||
```
|
||||
65
docs/content/zh/deployment/configuration/storages.md
Normal file
65
docs/content/zh/deployment/configuration/storages.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: "存储端配置"
|
||||
---
|
||||
|
||||
# 存储端配置
|
||||
|
||||
请先阅读 [配置说明](../) 了解配置文件的基本格式.
|
||||
|
||||
## Alist
|
||||
|
||||
`type=alist`
|
||||
|
||||
不支持 Stream 模式.
|
||||
|
||||
```toml
|
||||
url = "https://alist.example.com" # Alist 的 URL
|
||||
username = "your_username" # Alist 的用户名
|
||||
password = "your_password" # Alist 的密码
|
||||
base_path = "/path/saveanybot" # Alist 中的基础路径, 所有文件将存储在此路径下
|
||||
token_exp = 3600 # Alist 访问令牌的自动刷新时间, 单位秒
|
||||
token = "your_token"
|
||||
# Alist 的访问令牌, 可选, 如果不设置则使用用户名和密码进行身份验证.
|
||||
# 使用 token 验证时无法自动刷新 token
|
||||
```
|
||||
|
||||
## 本地磁盘
|
||||
|
||||
`type=local`
|
||||
|
||||
```toml
|
||||
base_path = "./downloads" # 本地存储的基础路径, 所有文件将存储在此路径下
|
||||
```
|
||||
|
||||
## WebDAV
|
||||
`type=webdav`
|
||||
|
||||
```toml
|
||||
url = "https://webdav.example.com" # WebDAV 的 URL
|
||||
username = "your_username" # WebDAV
|
||||
password = "your_password" # WebDAV 的密码
|
||||
base_path = "/path/to/webdav" # WebDAV 中的基础路径, 所有文件将存储在此路径下
|
||||
```
|
||||
|
||||
## MinIO (S3)
|
||||
|
||||
`type=minio`
|
||||
|
||||
```toml
|
||||
endpoint = "minio.example.com" # MinIO 或 S3 的端点
|
||||
access_key_id = "your_access_key_id" # MinIO 或 S3 的访问密钥 ID
|
||||
secret_access_key = "your_secret_access_key" # MinIO 或 S3 的秘密访问密钥
|
||||
bucket_name = "your_bucket_name" # MinIO 或 S3 的存储桶名称
|
||||
use_ssl = true # 是否使用 SSL, 默认为 true
|
||||
base_path = "/path/to/minio" # MinIO 中的基础路径, 所有文件将存储在此路径下
|
||||
```
|
||||
|
||||
## Telegram
|
||||
|
||||
`type=telegram`
|
||||
|
||||
不支持 Stream 模式.
|
||||
|
||||
```toml
|
||||
chat_id = "123456789" # Telegram 聊天 ID, Bot 将把文件发送到这个聊天
|
||||
```
|
||||
145
docs/content/zh/deployment/installation.md
Normal file
145
docs/content/zh/deployment/installation.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
title: "安装与更新"
|
||||
---
|
||||
|
||||
# 安装与更新
|
||||
|
||||
## 从预编译文件部署
|
||||
|
||||
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
|
||||
|
||||
在解压后目录新建 `config.toml` 文件, 参考 [配置说明](../configuration) 编辑配置文件
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
chmod +x saveany-bot
|
||||
./saveany-bot
|
||||
```
|
||||
|
||||
### 进程守护
|
||||
|
||||
{{< tabs "daemon" >}}
|
||||
{{< tab "systemd (常规 Linux)" >}}
|
||||
|
||||
创建文件 <code>/etc/systemd/system/saveany-bot.service</code> 并写入以下内容:
|
||||
|
||||
{{< codeblock >}}
|
||||
[Unit]
|
||||
Description=SaveAnyBot
|
||||
After=systemd-user-sessions.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/yourpath/
|
||||
ExecStart=/yourpath/saveany-bot
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
{{< /codeblock >}}
|
||||
|
||||
设为开机启动并启动服务:
|
||||
|
||||
{{< codeblock >}}
|
||||
systemctl enable --now saveany-bot
|
||||
{{< /codeblock >}}
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab "procd (OpenWrt)" >}}
|
||||
|
||||
<h4>添加开机自启动服务</h4>
|
||||
|
||||
创建文件 <code>/etc/init.d/saveanybot</code> ,参考 <a href="https://github.com/krau/SaveAny-Bot/blob/main/docs/confs/wrt_init" target="_blank">wrt_init</a> 并自行修改:
|
||||
|
||||
{{< codeblock >}}
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
#This is the OpenWRT init.d script for SaveAnyBot
|
||||
|
||||
START=99
|
||||
STOP=10
|
||||
description="SaveAnyBot"
|
||||
|
||||
WORKING_DIR="/mnt/mmc1-1/SaveAnyBot"
|
||||
EXEC_PATH="$WORKING_DIR/saveany-bot"
|
||||
start() {
|
||||
echo "Starting SaveAnyBot..."
|
||||
cd $WORKING_DIR
|
||||
$EXEC_PATH &
|
||||
}
|
||||
stop() {
|
||||
echo "Stopping SaveAnyBot..."
|
||||
killall saveany-bot
|
||||
}
|
||||
reload() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
{{< /codeblock >}}
|
||||
|
||||
赋予权限:
|
||||
|
||||
{{< codeblock >}}
|
||||
chmod +x /etc/init.d/saveanybot
|
||||
{{< /codeblock >}}
|
||||
|
||||
然后将文件复制到 <code>/etc/rc.d</code> 并重命名为 <code>S99saveanybot</code>, 同样赋予权限:
|
||||
|
||||
{{< codeblock >}}
|
||||
chmod +x /etc/rc.d/S99saveanybot
|
||||
{{< /codeblock >}}
|
||||
|
||||
<h4>添加快捷指令</h4>
|
||||
|
||||
创建文件 <code>/usr/bin/sabot</code> ,参考 <a href="https://github.com/krau/SaveAny-Bot/blob/main/docs/confs/wrt_bin" target="_blank">wrt_bin</a> 并自行修改,注意此处文件编码仅支持 ANSI 936 .
|
||||
|
||||
随后赋予权限:
|
||||
|
||||
{{< codeblock >}}
|
||||
chmod +x /usr/bin/sabot
|
||||
{{< /codeblock >}}
|
||||
|
||||
使用: <code>sudo sabot start|stop|restart|status|enable|disable</code>
|
||||
|
||||
{{< /tab >}}
|
||||
{{< /tabs >}}
|
||||
|
||||
|
||||
## 使用 Docker 部署
|
||||
|
||||
### Docker Compose
|
||||
|
||||
下载 [docker-compose.yml](https://github.com/krau/SaveAny-Bot/blob/main/docker-compose.yml) 文件, 在同目录下新建 `config.toml` 文件, 参考 [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
||||
|
||||
启动:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```shell
|
||||
docker run -d --name saveany-bot \
|
||||
-v /path/to/config.toml:/app/config.toml \
|
||||
-v /path/to/downloads:/app/downloads \
|
||||
ghcr.io/krau/saveany-bot:latest
|
||||
```
|
||||
|
||||
## 更新
|
||||
|
||||
使用 `upgrade` 或 `up` 升级到最新版
|
||||
|
||||
```bash
|
||||
./saveany-bot upgrade
|
||||
```
|
||||
|
||||
如果是 Docker 部署, 使用以下命令更新:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/krau/saveany-bot:latest
|
||||
docker restart saveany-bot
|
||||
```
|
||||
@@ -1,3 +1,8 @@
|
||||
---
|
||||
title: "常见问题"
|
||||
weight: 15
|
||||
---
|
||||
|
||||
# 常见问题
|
||||
|
||||
## 上传 alist 失败也会显示成功
|
||||
@@ -6,11 +11,8 @@
|
||||
|
||||
## Bot 提示下载成功但是 alist 未显示
|
||||
|
||||
alist 缓存了目录结构, 参考文档可以调整缓存时间
|
||||
|
||||
https://alist.nn.ci/zh/guide/drivers/common.html#缓存过期
|
||||
alist 缓存了目录结构, 参考 <a href="https://alist.nn.ci/zh/guide/drivers/common.html#缓存过期" target="_blank">文档</a> 可以调整缓存时间
|
||||
|
||||
## docker部署配置了代理后仍无法连接 telegram (初始化客户端超时)
|
||||
|
||||
docker 不能直接访问宿主机网络, 如果你不熟悉其用法, 请将容器设为 host 模式:
|
||||
|
||||
docker 不能直接访问宿主机网络, 如果你不熟悉其用法, 请将容器设为 host 模式.
|
||||
67
docs/content/zh/usage/_index.md
Normal file
67
docs/content/zh/usage/_index.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "使用帮助"
|
||||
weight: 10
|
||||
---
|
||||
|
||||
# 使用帮助
|
||||
|
||||
这里介绍 Save Any Bot 的一些功能和使用方法, 如果你没有在这里找到你需要的内容, 另请参阅 [配置说明](../deployment/configuration) 或前往 Github [Discussions](https://github.com/krau/SaveAny-Bot/discussions) 提问.
|
||||
|
||||
## 转存文件
|
||||
|
||||
Bot 接受两种消息: 文件和链接.
|
||||
|
||||
对于链接, 目前支持以下类型的链接:
|
||||
|
||||
1. Telegram 消息链接, 例如: `https://t.me/acherkrau/1097`. **即使频道禁止了转发和保存, Bot 依然可以下载其文件.**
|
||||
2. Telegra.ph 的文章链接, Bot 将下载其中的所有图片
|
||||
|
||||
## 静默模式 (silent)
|
||||
|
||||
使用 `/silent` 命令可以开关静默模式.
|
||||
|
||||
默认情况下不开启静默模式, Bot 会询问你每个文件的保存位置.
|
||||
|
||||
开启静默模式后, Bot 会直接保存文件到默认位置, 无需确认.
|
||||
|
||||
在开启静默模式之前, 需要使用 `/storage` 命令设置默认保存位置.
|
||||
|
||||
|
||||
## 存储规则
|
||||
|
||||
允许你为 Bot 在上传文件到存储时设置一些重定向规则, 用于自动整理所保存的文件.
|
||||
|
||||
见: <a href="https://github.com/krau/SaveAny-Bot/issues/28" target="_blank">#28</a>
|
||||
|
||||
目前支持的规则类型:
|
||||
|
||||
1. FILENAME-REGEX
|
||||
2. MESSAGE-REGEX
|
||||
|
||||
添加规则的基本语法:
|
||||
|
||||
"规则类型 规则内容 存储名 路径"
|
||||
|
||||
注意空格的使用, 语法正确 bot 才能解析, 以下是一条合法的添加规则命令:
|
||||
|
||||
```
|
||||
/rule add FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /视频
|
||||
```
|
||||
|
||||
此外, 规则中的存储名若使用 "CHOSEN" , 则表示存储到点击按钮选择的存储端的路径下
|
||||
|
||||
规则介绍:
|
||||
|
||||
### FILENAME-REGEX
|
||||
|
||||
根据文件名正则匹配, 规则内容要求为一个合法的正则表达式, 如
|
||||
|
||||
```
|
||||
FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /视频
|
||||
```
|
||||
|
||||
表示将文件名后缀为 mp4,mkv,ts,avi,flv 的文件放到名为 MyAlist 存储下的 /视频 目录内 (同时受配置文件中的 `base_path` 影响)
|
||||
|
||||
### MESSAGE-REGEX
|
||||
|
||||
同上, 但是是根据消息本身的文本内容正则匹配
|
||||
@@ -1,11 +0,0 @@
|
||||
# 参与开发
|
||||
|
||||
## 贡献新存储端
|
||||
|
||||
1. Fork 本项目, 克隆到本地
|
||||
2. 在 `config/storage` 目录下定义存储端配置, 并添加到 `config/storage/factory.go` 中
|
||||
3. 在 `types/types.go` 中添加新的存储端类型
|
||||
4. 在 `storage` 目录下新建一个包, 编写存储端实现, 然后在 `storage/storage.go` 中导入并添加它
|
||||
5. 更新 `config.example.toml` 文件, 添加新的示例配置
|
||||
|
||||
*可能确实有点麻烦了 = =*
|
||||
@@ -1,94 +0,0 @@
|
||||
# 部署指南
|
||||
|
||||
## 从二进制文件部署
|
||||
|
||||
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
|
||||
|
||||
在解压后目录新建 `config.toml` 文件, 参考 [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
chmod +x saveany-bot
|
||||
./saveany-bot
|
||||
```
|
||||
|
||||
### 添加为 systemd 服务
|
||||
|
||||
创建文件 `/etc/systemd/system/saveany-bot.service` 并写入以下内容:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=SaveAnyBot
|
||||
After=systemd-user-sessions.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/yourpath/
|
||||
ExecStart=/yourpath/saveany-bot
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
设为开机启动并启动服务:
|
||||
|
||||
```bash
|
||||
systemctl enable --now saveany-bot
|
||||
```
|
||||
|
||||
### 为OpenWrt及衍生系统添加开机自启动服务
|
||||
|
||||
创建文件 ` /etc/init.d/saveanybot` ,参考[saveanybot](https://github.com/krau/SaveAny-Bot/blob/main/docs/saveanybot)自行修改.
|
||||
|
||||
`chmod +x /etc/init.d/saveanybot`
|
||||
|
||||
完成后,将文件复制到 `/etc/rc.d`并重命名为`S99saveanybot`.
|
||||
|
||||
`chmod +x /etc/rc.d/S99saveanybot`
|
||||
|
||||
### 为OpenWrt及衍生系统添加快捷指令
|
||||
|
||||
创建文件` /usr/bin/sabot` ,参考[sabot](https://github.com/krau/SaveAny-Bot/blob/main/docs/sabot)自行配置修改,注意此处文件编码仅支持 ANSI 936 .
|
||||
|
||||
`chmod +x /usr/bin/sabot`
|
||||
|
||||
之后,终端输入`sabot start|stop|restart|status|enable|disable`即可.
|
||||
|
||||
|
||||
## 使用 Docker 部署
|
||||
|
||||
### Docker Compose
|
||||
|
||||
下载 [docker-compose.yml](https://github.com/krau/SaveAny-Bot/blob/main/docker-compose.yml) 文件, 在同目录下新建 `config.toml` 文件, 参考 [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
||||
|
||||
启动:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```shell
|
||||
docker run -d --name saveany-bot \
|
||||
-v /path/to/config.toml:/app/config.toml \
|
||||
-v /path/to/downloads:/app/downloads \
|
||||
ghcr.io/krau/saveany-bot:latest
|
||||
```
|
||||
|
||||
## 更新
|
||||
|
||||
使用 `upgrade` 或 `up` 升级到最新版
|
||||
|
||||
```bash
|
||||
./saveany-bot upgrade
|
||||
```
|
||||
|
||||
如果是 Docker 部署, 使用以下命令更新:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/krau/saveany-bot:latest
|
||||
docker restart saveany-bot
|
||||
```
|
||||
@@ -1,46 +0,0 @@
|
||||
# 实验性功能
|
||||
|
||||
这里的功能不太稳定, 且未来可能会被删除或修改。
|
||||
|
||||
## 存储规则
|
||||
|
||||
允许你为 Bot 在上传文件到存储时设置一些重定向规则, 用于自动整理所保存的文件.
|
||||
|
||||
见: https://github.com/krau/SaveAny-Bot/issues/28
|
||||
|
||||
目前支持的规则类型:
|
||||
|
||||
1. FILENAME-REGEX
|
||||
2. MESSAGE-REGEX
|
||||
|
||||
添加规则的基本语法:
|
||||
|
||||
"规则类型 规则内容 存储名 路径"
|
||||
|
||||
注意空格的使用, 语法正确 bot 才能解析, 以下是一条合法的添加规则命令:
|
||||
|
||||
```
|
||||
/rule add FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /视频
|
||||
```
|
||||
|
||||
此外, 规则中的存储名若使用 "CHOSEN" , 则表示存储到点击按钮选择的存储端的路径下
|
||||
|
||||
规则介绍:
|
||||
|
||||
### FILENAME-REGEX
|
||||
|
||||
根据文件名正则匹配, 规则内容要求为一个合法的正则表达式, 如
|
||||
|
||||
```
|
||||
FILENAME-REGEX (?i)\.(mp4|mkv|ts|avi|flv)$ MyAlist /视频
|
||||
```
|
||||
|
||||
表示将文件名后缀为 mp4,mkv,ts,avi,flv 的文件放到名为 MyAlist 存储下的 /视频 目录内 (同时受配置文件中的 `base_path` 影响)
|
||||
|
||||
### MESSAGE-REGEX
|
||||
|
||||
同上, 根据消息文本内容正则匹配
|
||||
|
||||
## 复制并发送媒体消息
|
||||
|
||||
将接收到的文件(媒体)消息, 或链接对应的消息原样发送到当前聊天, 点击选择存储按钮中的 "发送到当前聊天" 即可.
|
||||
@@ -1,38 +0,0 @@
|
||||
# 使用帮助
|
||||
|
||||
## 保存文件
|
||||
|
||||
Bot 接受两种消息: 文件和链接.
|
||||
|
||||
支持以下链接:
|
||||
|
||||
1. 公开频道 (具有用户名) 的消息链接, 例如: `https://t.me/acherkrau/1097`. **即使频道禁止了转发和保存, Bot 依然可以下载其文件.**
|
||||
2. Telegra.ph 的文章链接, Bot 将下载其中的所有图片
|
||||
|
||||
## 静默模式 (silent)
|
||||
|
||||
使用 `/silent` 命令可以开关静默模式.
|
||||
|
||||
默认情况下不开启静默模式, Bot 会询问你每个文件的保存位置.
|
||||
|
||||
开启静默模式后, Bot 会直接保存文件到默认位置, 无需确认.
|
||||
|
||||
在开启静默模式之前, 需要使用 `/storage` 命令设置默认保存位置.
|
||||
|
||||
## Stream 模式
|
||||
|
||||
在配置文件中将 `stream` 设置为 `true` 可以开启 Stream 模式.
|
||||
|
||||
未开启时, Bot 处理任务分为两步: 下载和上传. Bot 会将文件暂存到本地, 然后上传到对应存储位置, 最后删除本地文件.
|
||||
|
||||
开启后, Bot 将直接将文件流式传输到存储端, 不需要下载到本地.
|
||||
|
||||
该功能对于硬盘空间有限的部署环境十分有用, 然而相较于普通模式也具有一些弊端:
|
||||
|
||||
- 无法使用多线程从 telegram 下载文件, 速度较慢.
|
||||
- 网络不稳定时, 任务失败率高.
|
||||
- 无法在中间层对文件进行处理, 例如自动文件类型识别.
|
||||
|
||||
**不支持** Stream 模式的存储端:
|
||||
|
||||
- alist
|
||||
@@ -1,7 +0,0 @@
|
||||
# SaveAnyBot 文档
|
||||
|
||||
SaveAnyBot 是一个可以保存 Telegram 上的文件到云存储的机器人, 就像 PikPak Bot 一样.
|
||||
|
||||
不同的是, SaveAnyBot 提供更灵活的存储端选择, 并实现一些更强大的功能.
|
||||
|
||||
本项目以 AGPL-3.0 协议开源, 请遵守协议使用.
|
||||
5
docs/go.mod
Normal file
5
docs/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/krau/SaveAny-Bot/docs
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require github.com/alex-shpak/hugo-book v0.0.0-20250530233833-f2c703e15588 // indirect
|
||||
2
docs/go.sum
Normal file
2
docs/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/alex-shpak/hugo-book v0.0.0-20250530233833-f2c703e15588 h1:pwxkzpzw/iJSxMBgQLWjYMQubhIemLG3UrNjeWoCkSM=
|
||||
github.com/alex-shpak/hugo-book v0.0.0-20250530233833-f2c703e15588/go.mod h1:L4NMyzbn15fpLIpmmtDg9ZFFyTZzw87/lk7M2bMQ7ds=
|
||||
47
docs/hugo.toml
Normal file
47
docs/hugo.toml
Normal file
@@ -0,0 +1,47 @@
|
||||
baseURL = 'https://sabot.unv.app/'
|
||||
title = 'Save Any Bot'
|
||||
disablePathToLower = true
|
||||
enableGitInfo = true
|
||||
defaultContentLanguage = 'zh'
|
||||
|
||||
|
||||
[module]
|
||||
[[module.imports]]
|
||||
path = 'github.com/alex-shpak/hugo-book'
|
||||
|
||||
[params]
|
||||
BookTheme = 'auto'
|
||||
BookToC = true
|
||||
BookLogo = 'logo.png'
|
||||
BookSection = '*'
|
||||
BookRepo = 'https://github.com/krau/saveany-bot'
|
||||
BookCommitPath = 'commit'
|
||||
BookEditPath = 'edit/main/docs'
|
||||
BookDateFormat = '2006/01/02'
|
||||
BookSearch = false
|
||||
|
||||
[languages]
|
||||
|
||||
[languages.zh]
|
||||
languageName = "简体中文"
|
||||
contentDir = "content/zh"
|
||||
weight = 1
|
||||
|
||||
[[languages.zh.menu.before]]
|
||||
name = "🔗 GitHub"
|
||||
url = "https://github.com/krau/SaveAny-Bot"
|
||||
weight = 10
|
||||
|
||||
[languages.en]
|
||||
languageName = "English"
|
||||
contentDir = "content/en"
|
||||
weight = 2
|
||||
|
||||
[[languages.en.menu.before]]
|
||||
name = "🔗 GitHub"
|
||||
url = "https://github.com/krau/SaveAny-Bot"
|
||||
weight = 10
|
||||
|
||||
[markup]
|
||||
[markup.goldmark.renderer]
|
||||
unsafe = true
|
||||
3
docs/layouts/shortcodes/codeblock.html
Normal file
3
docs/layouts/shortcodes/codeblock.html
Normal file
@@ -0,0 +1,3 @@
|
||||
{{- $lang := .Get "lang" | default "text" -}}
|
||||
{{- $content := .Inner | strings.TrimSpace | htmlEscape -}}
|
||||
<pre><code class="language-{{ $lang }}">{{ $content }}</code></pre>
|
||||
@@ -1,35 +0,0 @@
|
||||
site_name: SaveAnyBot 官方文档
|
||||
site_author: Krau
|
||||
site_description: SaveAnyBot 是一个可以保存 Telegram 上的文件到多种云存储的机器人, 本文档将帮助你了解如何部署和使用它.
|
||||
repo_name: krau/saveany-bot
|
||||
repo_url: https://github.com/krau/saveany-bot
|
||||
copyright: CC BY-NC-SA 4.0
|
||||
theme:
|
||||
name: material
|
||||
language: zh
|
||||
highlightjs: true
|
||||
palette:
|
||||
- media: "(prefers-color-scheme)"
|
||||
toggle:
|
||||
icon: material/brightness-auto
|
||||
name: 切换主题
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: indigo
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: 暗色模式
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: blue grey
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: 亮色模式
|
||||
|
||||
nav:
|
||||
- index.md
|
||||
- deploy.md
|
||||
- help.md
|
||||
- experimental.md
|
||||
- faq.md
|
||||
- contribute.md
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"Target":"book.min.a22f4c7d8c2bdc5e3d6e34ba11cb59ab50ea5772594e71305bfd5a595dc78b7e.css","MediaType":"text/css","Data":{"Integrity":"sha256-oi9MfYwr3F49bjS6EctZq1DqV3JZTnEwW/1aWV3Hi34="}}
|
||||
0
docs/logo.jpg → docs/static/logo.png
vendored
0
docs/logo.jpg → docs/static/logo.png
vendored
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
5
pkg/enums/ctxkey/context_key.go
Normal file
5
pkg/enums/ctxkey/context_key.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package ctxkey
|
||||
|
||||
//go:generate go-enum --values --names --flag --nocase --noprefix
|
||||
// ENUM(content-length)
|
||||
type ContextKey string
|
||||
@@ -4,7 +4,7 @@
|
||||
// Build Date: 2025-03-18T23:42:14Z
|
||||
// Built By: goreleaser
|
||||
|
||||
package key
|
||||
package ctxkey
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,14 +12,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// ContextKeyContentLength is a ContextKey of type content-length.
|
||||
ContextKeyContentLength ContextKey = "content-length"
|
||||
// ContentLength is a ContextKey of type content-length.
|
||||
ContentLength ContextKey = "content-length"
|
||||
)
|
||||
|
||||
var ErrInvalidContextKey = fmt.Errorf("not a valid ContextKey, try [%s]", strings.Join(_ContextKeyNames, ", "))
|
||||
|
||||
var _ContextKeyNames = []string{
|
||||
string(ContextKeyContentLength),
|
||||
string(ContentLength),
|
||||
}
|
||||
|
||||
// ContextKeyNames returns a list of possible string values of ContextKey.
|
||||
@@ -32,7 +32,7 @@ func ContextKeyNames() []string {
|
||||
// ContextKeyValues returns a list of the values for ContextKey
|
||||
func ContextKeyValues() []ContextKey {
|
||||
return []ContextKey{
|
||||
ContextKeyContentLength,
|
||||
ContentLength,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func (x ContextKey) IsValid() bool {
|
||||
}
|
||||
|
||||
var _ContextKeyValue = map[string]ContextKey{
|
||||
"content-length": ContextKeyContentLength,
|
||||
"content-length": ContentLength,
|
||||
}
|
||||
|
||||
// ParseContextKey attempts to convert a string to a ContextKey.
|
||||
@@ -1,5 +0,0 @@
|
||||
package key
|
||||
|
||||
//go:generate go-enum --values --names --flag --nocase
|
||||
// ENUM(content-length)
|
||||
type ContextKey string
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
config "github.com/krau/SaveAny-Bot/config/storage"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/key"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||
storenum "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
||||
)
|
||||
|
||||
@@ -118,7 +118,7 @@ func (a *Alist) Save(ctx context.Context, reader io.Reader, storagePath string)
|
||||
req.Header.Set("Authorization", a.token)
|
||||
req.Header.Set("File-Path", url.PathEscape(candidate))
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
if length := ctx.Value(key.ContextKeyContentLength); length != nil {
|
||||
if length := ctx.Value(ctxkey.ContentLength); length != nil {
|
||||
length, ok := length.(int64)
|
||||
if ok {
|
||||
req.ContentLength = length
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
config "github.com/krau/SaveAny-Bot/config/storage"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||
storenum "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
@@ -72,8 +73,14 @@ func (m *Minio) Save(ctx context.Context, r io.Reader, storagePath string) error
|
||||
for i := 1; m.Exists(ctx, candidate); i++ {
|
||||
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
||||
}
|
||||
|
||||
_, err := m.client.PutObject(ctx, m.config.BucketName, candidate, r, -1, minio.PutObjectOptions{})
|
||||
size := int64(-1)
|
||||
if length := ctx.Value(ctxkey.ContentLength); length != nil {
|
||||
length, ok := length.(int64)
|
||||
if ok && length > 0 {
|
||||
size = length
|
||||
}
|
||||
}
|
||||
_, err := m.client.PutObject(ctx, m.config.BucketName, candidate, r, size, minio.PutObjectOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload file to minio: %w", err)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/key"
|
||||
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -54,7 +54,7 @@ func (c *Client) doRequest(ctx context.Context, method WebdavMethod, url string,
|
||||
req.Header.Set("Depth", "1")
|
||||
}
|
||||
if method == WebdavMethodPut && ctx != nil {
|
||||
if length := ctx.Value(key.ContextKeyContentLength); length != nil {
|
||||
if length := ctx.Value(ctxkey.ContentLength); length != nil {
|
||||
if l, ok := length.(int64); ok {
|
||||
req.ContentLength = l
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user