mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-05-10 17:52:44 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ef7c5ce60 | ||
|
|
03c833acbf | ||
|
|
0d369c7c79 | ||
|
|
7a288e3155 | ||
|
|
f9d51a688c | ||
|
|
dcc3f5f99b | ||
|
|
a04c46fab8 | ||
|
|
b3c59fde72 | ||
|
|
d1d284eedc |
28
README.md
28
README.md
@@ -1 +1,27 @@
|
|||||||
# Save Any Bot
|
# Save Any Bot
|
||||||
|
|
||||||
|
把 Telegram 的文件保存到各类存储端.
|
||||||
|
|
||||||
|
> *就像 PikPak Bot 一样*
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
在 [Release](https://github.com/krau/SaveAny-Bot/releases) 页面下载对应平台的二进制文件.
|
||||||
|
|
||||||
|
在解压后目录新建 `config.toml` 文件, 参考 [config.toml.example](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) 编辑配置文件.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> 由于 Telegram 官方 Bot API 的限制, Bot 无法下载大于 20MB 的文件. 你需要部署一个本地的 Telegram Bot API 来解决这个问题, 然后将配置文件中的 telegram.api 改为你自己的 api 地址.
|
||||||
|
>
|
||||||
|
> 参考: [telegram-bot-api-compose](https://github.com/krau/telegram-bot-api-compose)
|
||||||
|
|
||||||
|
运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x saveany-bot
|
||||||
|
./saveany-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
向 Bot 发送(转发)文件, 按照提示操作.
|
||||||
@@ -3,6 +3,7 @@ package bot
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -151,7 +152,8 @@ func HandleFileMessage(ctx context.Context, bot *telego.Bot, message telego.Mess
|
|||||||
fileName = message.Audio.FileName
|
fileName = message.Audio.FileName
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileID == "" || fileName == "" {
|
if fileID == "" {
|
||||||
|
logger.L.Error("File ID is empty")
|
||||||
ReplyMessage(message, "文件信息获取失败")
|
ReplyMessage(message, "文件信息获取失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -182,6 +184,9 @@ func HandleFileMessage(ctx context.Context, bot *telego.Bot, message telego.Mess
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if fileName == "" {
|
||||||
|
fileName = filepath.Base(file.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
err = dao.AddReceivedFile(&model.ReceivedFile{
|
err = dao.AddReceivedFile(&model.ReceivedFile{
|
||||||
FileUniqueID: file.FileUniqueID,
|
FileUniqueID: file.FileUniqueID,
|
||||||
|
|||||||
19
bot/utils.go
19
bot/utils.go
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
"github.com/mymmrac/telego/telegoutil"
|
"github.com/mymmrac/telego/telegoutil"
|
||||||
)
|
)
|
||||||
@@ -20,15 +20,18 @@ func ReplyMessage(replyTo telego.Message, format string, args ...any) (*telego.M
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var StorageDisplayNames = map[string]string{
|
||||||
|
"all": "全部",
|
||||||
|
"local": "服务器磁盘",
|
||||||
|
"alist": "Alist",
|
||||||
|
"webdav": "WebDAV",
|
||||||
|
}
|
||||||
|
|
||||||
func AddTaskReplyMarkup(messageID int) *telego.InlineKeyboardMarkup {
|
func AddTaskReplyMarkup(messageID int) *telego.InlineKeyboardMarkup {
|
||||||
storageButtons := make([]telego.InlineKeyboardButton, 0)
|
storageButtons := make([]telego.InlineKeyboardButton, 0)
|
||||||
if config.Cfg.Storage.Local.Enable {
|
for name := range storage.Storages {
|
||||||
storageButtons = append(storageButtons, telegoutil.InlineKeyboardButton("服务器磁盘").
|
storageButtons = append(storageButtons, telegoutil.InlineKeyboardButton(StorageDisplayNames[string(name)]).
|
||||||
WithCallbackData(fmt.Sprintf("add %d local", messageID)))
|
WithCallbackData(fmt.Sprintf("add %d %s", messageID, name)))
|
||||||
}
|
|
||||||
if config.Cfg.Storage.Alist.Enable {
|
|
||||||
storageButtons = append(storageButtons, telegoutil.InlineKeyboardButton("Alist").
|
|
||||||
WithCallbackData(fmt.Sprintf("add %d alist", messageID)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(storageButtons) > 1 {
|
if len(storageButtons) > 1 {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/imroc/req/v3"
|
"github.com/imroc/req/v3"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
@@ -10,7 +11,7 @@ import (
|
|||||||
var ReqClient *req.Client
|
var ReqClient *req.Client
|
||||||
|
|
||||||
func initClient() {
|
func initClient() {
|
||||||
ReqClient = req.NewClient().SetOutputDirectory(config.Cfg.Temp.BasePath)
|
ReqClient = req.NewClient().SetOutputDirectory(config.Cfg.Temp.BasePath).SetTimeout(86400 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDownloadedFilePath(filename string) string {
|
func GetDownloadedFilePath(filename string) string {
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
[telegram]
|
[telegram]
|
||||||
token = ""
|
token = "" # Bot Token
|
||||||
admins = []
|
admins = [777000] # 你的 user_id
|
||||||
|
api = "https://api.telegram.org"
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
level = "DEBUG"
|
level = "DEBUG" # 日志等级
|
||||||
|
|
||||||
[temp]
|
[temp]
|
||||||
base_path = "cache/"
|
base_path = "cache/" # 临时目录, 请不要在此目录下存放任何其他文件
|
||||||
cache_ttl = 30
|
cache_ttl = 30 # 临时文件保存时间, 单位: 秒
|
||||||
|
|
||||||
|
[db]
|
||||||
|
path = "data/data.db" # 数据库文件路径
|
||||||
|
|
||||||
[storage]
|
[storage]
|
||||||
[storage.alist]
|
[storage.alist] # Alist
|
||||||
|
enable = true
|
||||||
|
base_path = "/telegram" # 保存路径
|
||||||
|
username = "admin" # 用户名
|
||||||
|
password = "password" # 密码
|
||||||
|
url = "https://alist.com" # Alist 地址
|
||||||
|
token_exp = 86400 # token 过期时间, 单位: 秒
|
||||||
|
|
||||||
|
[storage.local] # 本地磁盘
|
||||||
|
enable = true
|
||||||
|
base_path = "downloads/" # 保存路径
|
||||||
|
|
||||||
|
[storage.webdav] # WebDav
|
||||||
enable = true
|
enable = true
|
||||||
base_path = "/telegram"
|
base_path = "/telegram"
|
||||||
username = "admin"
|
username = "admin"
|
||||||
password = "password"
|
password = "password"
|
||||||
url = "https://alist.com"
|
url = "https://alist.com/dav"
|
||||||
token_exp = 86400
|
|
||||||
|
|
||||||
[storage.local]
|
|
||||||
enable = true
|
|
||||||
base_path = "downloads/"
|
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ type telegramConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type storageConfig struct {
|
type storageConfig struct {
|
||||||
Alist alistConfig `toml:"alist" mapstructure:"alist"`
|
Alist alistConfig `toml:"alist" mapstructure:"alist"`
|
||||||
Local localConfig `toml:"local" mapstructure:"local"`
|
Local localConfig `toml:"local" mapstructure:"local"`
|
||||||
|
Webdav webdavConfig `toml:"webdav" mapstructure:"webdav"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type alistConfig struct {
|
type alistConfig struct {
|
||||||
@@ -55,6 +56,14 @@ type localConfig struct {
|
|||||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type webdavConfig struct {
|
||||||
|
Enable bool `toml:"enable" mapstructure:"enable"`
|
||||||
|
URL string `toml:"url" mapstructure:"url"`
|
||||||
|
Username string `toml:"username" mapstructure:"username"`
|
||||||
|
Password string `toml:"password" mapstructure:"password"`
|
||||||
|
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
||||||
|
}
|
||||||
|
|
||||||
var Cfg *Config
|
var Cfg *Config
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func processPendingTask(task types.Task) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = common.ReqClient.R().SetContext(task.Ctx).SetOutputFile(task.FileName).SetDownloadCallbackWithInterval(func(info req.DownloadInfo) {
|
_, err = common.ReqClient.R().SetOutputFile(task.FileName).SetDownloadCallbackWithInterval(func(info req.DownloadInfo) {
|
||||||
if info.Response == nil || info.Response.Response == nil || info.Response.Response.StatusCode != 200 {
|
if info.Response == nil || info.Response.Response == nil || info.Response.Response.StatusCode != 200 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -10,6 +10,7 @@ require (
|
|||||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
|
github.com/studio-b12/gowebdav v0.9.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -158,6 +158,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
|
||||||
|
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func refreshToken(client *req.Client) {
|
|||||||
func (a *Alist) Init() {
|
func (a *Alist) Init() {
|
||||||
basePath = config.Cfg.Storage.Alist.BasePath
|
basePath = config.Cfg.Storage.Alist.BasePath
|
||||||
baseUrl = config.Cfg.Storage.Alist.URL
|
baseUrl = config.Cfg.Storage.Alist.URL
|
||||||
reqClient = req.C().SetTLSHandshakeTimeout(time.Second * 10).SetBaseURL(baseUrl)
|
reqClient = req.C().SetTLSHandshakeTimeout(time.Second * 10).SetBaseURL(baseUrl).SetTimeout(time.Hour * 24)
|
||||||
loginReq = &loginRequset{
|
loginReq = &loginRequset{
|
||||||
Username: config.Cfg.Storage.Alist.Username,
|
Username: config.Cfg.Storage.Alist.Username,
|
||||||
Password: config.Cfg.Storage.Alist.Password,
|
Password: config.Cfg.Storage.Alist.Password,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/logger"
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
"github.com/krau/SaveAny-Bot/storage/alist"
|
"github.com/krau/SaveAny-Bot/storage/alist"
|
||||||
"github.com/krau/SaveAny-Bot/storage/local"
|
"github.com/krau/SaveAny-Bot/storage/local"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage/webdav"
|
||||||
"github.com/krau/SaveAny-Bot/types"
|
"github.com/krau/SaveAny-Bot/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ func Init() {
|
|||||||
Storages[types.Local] = new(local.Local)
|
Storages[types.Local] = new(local.Local)
|
||||||
Storages[types.Local].Init()
|
Storages[types.Local].Init()
|
||||||
}
|
}
|
||||||
|
if config.Cfg.Storage.Webdav.Enable {
|
||||||
|
Storages[types.Webdav] = new(webdav.Webdav)
|
||||||
|
Storages[types.Webdav].Init()
|
||||||
|
}
|
||||||
|
|
||||||
logger.L.Debug("Storage initialized")
|
logger.L.Debug("Storage initialized")
|
||||||
}
|
}
|
||||||
|
|||||||
50
storage/webdav/webdav.go
Normal file
50
storage/webdav/webdav.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package webdav
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/logger"
|
||||||
|
"github.com/studio-b12/gowebdav"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Webdav struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
Client *gowebdav.Client
|
||||||
|
basePath string
|
||||||
|
)
|
||||||
|
|
||||||
|
func (w *Webdav) Init() {
|
||||||
|
webdavConfig := config.Cfg.Storage.Webdav
|
||||||
|
basePath = strings.TrimSuffix(webdavConfig.BasePath, "/")
|
||||||
|
Client = gowebdav.NewClient(webdavConfig.URL, webdavConfig.Username, webdavConfig.Password)
|
||||||
|
if err := Client.Connect(); err != nil {
|
||||||
|
logger.L.Fatalf("Failed to connect to webdav server: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
Client.SetTimeout(24 * time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webdav) Save(ctx context.Context, filePath, storagePath string) error {
|
||||||
|
storagePath = filepath.Join(basePath, storagePath)
|
||||||
|
if err := Client.MkdirAll(filepath.Dir(storagePath), os.ModePerm); err != nil {
|
||||||
|
logger.L.Errorf("Failed to create directory %s: %v", filepath.Dir(storagePath), err)
|
||||||
|
return errors.New("webdav: failed to create directory")
|
||||||
|
}
|
||||||
|
fileBytes, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.L.Errorf("Failed to read file %s: %v", filePath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := Client.Write(storagePath, fileBytes, os.ModePerm); err != nil {
|
||||||
|
logger.L.Errorf("Failed to write file %s: %v", storagePath, err)
|
||||||
|
return errors.New("webdav: failed to write file")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -16,10 +16,11 @@ type StorageType string
|
|||||||
var (
|
var (
|
||||||
StorageAll StorageType = "all"
|
StorageAll StorageType = "all"
|
||||||
Local StorageType = "local"
|
Local StorageType = "local"
|
||||||
|
Webdav StorageType = "webdav"
|
||||||
Alist StorageType = "alist"
|
Alist StorageType = "alist"
|
||||||
)
|
)
|
||||||
|
|
||||||
var StorageTypes = []StorageType{Local, Alist, StorageAll}
|
var StorageTypes = []StorageType{Local, Alist, Webdav, StorageAll}
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
|
|||||||
Reference in New Issue
Block a user