Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9367419156 | ||
|
|
f80c4d7d55 | ||
|
|
ccfde34666 | ||
|
|
2b23446123 | ||
|
|
7882185ee1 | ||
|
|
2d17a731c4 |
@@ -1,6 +1,7 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -41,6 +42,11 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
ctx.Reply(update, ext.ReplyTextString("Failed to resolve chat ID"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if linkChat == nil {
|
||||
logger.L.Errorf("Cannot find chat: %s", chatUsername)
|
||||
ctx.Reply(update, ext.ReplyTextString("Cannot find chat"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
user, err := dao.GetUserByUserID(update.GetUserChat().GetID())
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to get user: %s", err)
|
||||
@@ -59,12 +65,10 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if file.FileName == "" {
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: "无法获取文件名",
|
||||
ID: replied.ID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
logger.L.Warnf("Empty file name, use generated name")
|
||||
file.FileName = fmt.Sprintf("%d_%d_%s", linkChat.GetID(), messageID, file.Hash())
|
||||
}
|
||||
|
||||
receivedFile := &types.ReceivedFile{
|
||||
Processing: false,
|
||||
FileName: file.FileName,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -74,6 +73,10 @@ Save Any Bot - 转存你的 Telegram 文件
|
||||
/path <存储类型> <路径> - 更改文件保存路径
|
||||
|
||||
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
|
||||
|
||||
默认存储位置: 在静默模式下保存到的位置
|
||||
|
||||
向 Bot 发送(转发)文件, 或发送一个公开频道的消息链接以保存文件
|
||||
`
|
||||
|
||||
func help(ctx *ext.Context, update *ext.Update) error {
|
||||
@@ -183,20 +186,14 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to get file from message: %s", err)
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: "获取文件失败: " + err.Error(),
|
||||
Message: fmt.Sprintf("获取文件失败: %s", err),
|
||||
ID: replied.ID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
if file.FileName == "" {
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: "无法获取文件名, 请使用 /save <自定义文件名> 回复此文件",
|
||||
ID: replied.ID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
file.FileName = fmt.Sprintf("%d_%d_%s", update.EffectiveChat().GetID(), replyToMsgID, file.Hash())
|
||||
}
|
||||
|
||||
receivedFile := &types.ReceivedFile{
|
||||
Processing: false,
|
||||
FileName: file.FileName,
|
||||
@@ -301,16 +298,11 @@ func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
file, err := FileFromMedia(media, "")
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to get file from media: %s", err)
|
||||
if errors.Is(err, ErrEmptyFileName) {
|
||||
ctx.Reply(update, ext.ReplyTextString("无法获取文件名, 请使用 /save <自定义文件名> 回复此文件"), nil)
|
||||
} else {
|
||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil)
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if file.FileName == "" {
|
||||
ctx.Reply(update, ext.ReplyTextString("无法获取文件名"), nil)
|
||||
return dispatcher.EndGroups
|
||||
file.FileName = fmt.Sprintf("%d_%d_%s", update.EffectiveChat().GetID(), update.EffectiveMessage.ID, file.Hash())
|
||||
}
|
||||
|
||||
if err := dao.SaveReceivedFile(&types.ReceivedFile{
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyFileName = errors.New("file name is empty")
|
||||
ErrEmptyDocument = errors.New("document is empty")
|
||||
ErrEmptyPhoto = errors.New("photo is empty")
|
||||
ErrEmptyPhotoSize = errors.New("photo size is empty")
|
||||
@@ -105,9 +104,6 @@ func FileFromMedia(media tg.MessageMediaClass, customFileName string) (*types.Fi
|
||||
break
|
||||
}
|
||||
}
|
||||
if fileName == "" {
|
||||
return nil, ErrEmptyFileName
|
||||
}
|
||||
return &types.File{
|
||||
Location: document.AsInputDocumentFileLocation(),
|
||||
FileSize: document.Size,
|
||||
|
||||
@@ -2,24 +2,20 @@ workers = 4 # 同时下载文件数
|
||||
retry = 3 # 下载失败重试次数
|
||||
|
||||
[telegram]
|
||||
token = "" # Bot Token
|
||||
admins = [777000] # 你的 user_id
|
||||
app_id = 123456 # Telegram API ID
|
||||
app_hash = "0123456789abcdef0123456789abcdef" # Telegram API Hash
|
||||
# Bot Token
|
||||
token = ""
|
||||
# 允许使用的用户 id 列表
|
||||
admins = [777000]
|
||||
# Telegram API 配置, 若不配置也可运行, 将使用默认的 API ID 和 API HASH
|
||||
# 推荐使用自己的 API ID 和 API HASH (https://my.telegram.org)
|
||||
# app_id = 123456
|
||||
# app_hash = "0123456789abcdef0123456789abcdef"
|
||||
|
||||
[telegram.proxy]
|
||||
# 启用代理连接 telegram, 只支持 socks5
|
||||
enable = false
|
||||
url = "socks5://127.0.0.1:7890" # 代理地址
|
||||
url = "socks5://127.0.0.1:7890"
|
||||
|
||||
[log]
|
||||
level = "DEBUG" # 日志等级
|
||||
|
||||
[temp]
|
||||
base_path = "cache/" # 下载文件临时目录, 请不要在此目录下存放任何其他文件
|
||||
cache_ttl = 30 # 临时文件保存时间, 单位: 秒
|
||||
|
||||
[db]
|
||||
path = "data/data.db" # 数据库文件路径
|
||||
|
||||
[storage]
|
||||
[storage.alist] # Alist
|
||||
@@ -29,6 +25,9 @@ username = "admin" # 用户名
|
||||
password = "password" # 密码
|
||||
url = "https://alist.com" # Alist 地址
|
||||
token_exp = 86400 # token 过期时间, 单位: 秒
|
||||
# 可直接使用 token 授权, 此时不能自动刷新登录信息
|
||||
# 配置 token 后, username , password , token_exp 将被忽略
|
||||
token = "jwt_token"
|
||||
|
||||
[storage.local] # 本地磁盘
|
||||
enable = true
|
||||
@@ -40,3 +39,15 @@ base_path = "/telegram"
|
||||
username = "admin"
|
||||
password = "password"
|
||||
url = "https://alist.com/dav"
|
||||
|
||||
|
||||
[log]
|
||||
# 日志等级
|
||||
level = "DEBUG"
|
||||
|
||||
[temp]
|
||||
base_path = "cache/" # 下载文件临时目录, 请不要在此目录下存放任何其他文件
|
||||
cache_ttl = 30 # 临时文件保存时间, 单位: 秒
|
||||
|
||||
[db]
|
||||
path = "data/data.db" # 数据库文件路径
|
||||
|
||||
@@ -59,6 +59,7 @@ type alistConfig struct {
|
||||
URL string `toml:"url" mapstructure:"url"`
|
||||
Username string `toml:"username" mapstructure:"username"`
|
||||
Password string `toml:"password" mapstructure:"password"`
|
||||
Token string `toml:"token" mapstructure:"token"`
|
||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
||||
TokenExp int64 `toml:"token_exp" mapstructure:"token_exp"`
|
||||
}
|
||||
@@ -106,6 +107,8 @@ func Init() {
|
||||
viper.SetDefault("storage.alist.base_path", "/")
|
||||
viper.SetDefault("storage.alist.token_exp", 3600)
|
||||
|
||||
viper.SafeWriteConfigAs("config.toml")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
fmt.Println("Error reading config file, ", err)
|
||||
os.Exit(1)
|
||||
|
||||
16
core/core.go
16
core/core.go
@@ -10,6 +10,8 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/duke-git/lancet/v2/fileutil"
|
||||
"github.com/gotd/td/tg"
|
||||
@@ -22,6 +24,9 @@ import (
|
||||
|
||||
func processPendingTask(task *types.Task) error {
|
||||
logger.L.Debugf("Start processing task: %s", task.String())
|
||||
if task.FileName() == "" {
|
||||
task.File.FileName = fmt.Sprintf("%d_%d_%s", task.FileChatID, task.FileMessageID, task.File.Hash())
|
||||
}
|
||||
cacheDestPath := filepath.Join(config.Cfg.Temp.BasePath, task.FileName())
|
||||
cacheDestPath, err := filepath.Abs(cacheDestPath)
|
||||
if err != nil {
|
||||
@@ -71,6 +76,7 @@ func processPendingTask(task *types.Task) error {
|
||||
Entities: entities,
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
|
||||
readCloser, err := NewTelegramReader(task.Ctx, bot.Client, &task.File.Location,
|
||||
0, task.File.FileSize-1, task.File.FileSize,
|
||||
progressCallback, task.File.FileSize/100)
|
||||
@@ -88,8 +94,16 @@ func processPendingTask(task *types.Task) error {
|
||||
if _, err := io.CopyN(dest, readCloser, task.File.FileSize); err != nil {
|
||||
return fmt.Errorf("failed to download file: %w", err)
|
||||
}
|
||||
|
||||
defer cleanCacheFile(cacheDestPath)
|
||||
if path.Ext(task.FileName()) == "" {
|
||||
mimeType, err := mimetype.DetectFile(cacheDestPath)
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to detect mime type: %s", err)
|
||||
} else {
|
||||
task.File.FileName = fmt.Sprintf("%s%s", task.FileName(), mimeType.Extension())
|
||||
task.StoragePath = fmt.Sprintf("%s%s", task.StoragePath, mimeType.Extension())
|
||||
}
|
||||
}
|
||||
|
||||
logger.L.Infof("Downloaded file: %s", cacheDestPath)
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
- SAVEANY_TELEGRAM_TOKEN=bot_token
|
||||
- SAVEANY_TELEGRAM_ADMINS=admin_id1,admin_id2
|
||||
# 推荐使用自己的 API ID 和 API HASH (https://my.telegram.org)
|
||||
# 若不配置将使用默认的 API ID 和 API HASH
|
||||
# 若不配置也可运行, 将使用默认的 API ID 和 API HASH
|
||||
# - SAVEANY_TELEGRAM_APP_ID=app_id
|
||||
# - SAVEANY_TELEGRAM_APP_HASH=app_hash
|
||||
|
||||
|
||||
1
go.mod
1
go.mod
@@ -24,6 +24,7 @@ require (
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -34,6 +34,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
|
||||
@@ -40,6 +40,15 @@ type loginResponse struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type meResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type putResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
@@ -110,6 +119,44 @@ func (a *Alist) Init() {
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
if config.Cfg.Storage.Alist.Token != "" {
|
||||
a.token = config.Cfg.Storage.Alist.Token
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+"/api/me", nil)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to create request: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
req.Header.Set("Authorization", a.token)
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to send request: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.L.Fatalf("Failed to get alist user info: %s", resp.Status)
|
||||
os.Exit(1)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to read response body: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var meResp meResponse
|
||||
if err := json.Unmarshal(body, &meResp); err != nil {
|
||||
logger.L.Fatalf("Failed to unmarshal me response: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if meResp.Code != http.StatusOK {
|
||||
logger.L.Fatalf("Failed to get alist user info: %s", meResp.Message)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.L.Debugf("Logged in Alist as %s", meResp.Data.Username)
|
||||
return
|
||||
}
|
||||
a.loginInfo = &loginRequest{
|
||||
Username: config.Cfg.Storage.Alist.Username,
|
||||
Password: config.Cfg.Storage.Alist.Password,
|
||||
|
||||
@@ -2,6 +2,8 @@ package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -56,3 +58,18 @@ type File struct {
|
||||
FileSize int64
|
||||
FileName string
|
||||
}
|
||||
|
||||
func (f File) Hash() string {
|
||||
locationBytes := []byte(f.Location.String())
|
||||
fileSizeBytes := []byte(fmt.Sprintf("%d", f.FileSize))
|
||||
fileNameBytes := []byte(f.FileName)
|
||||
|
||||
structBytes := append(locationBytes, fileSizeBytes...)
|
||||
structBytes = append(structBytes, fileNameBytes...)
|
||||
|
||||
hash := md5.New()
|
||||
hash.Write(structBytes)
|
||||
hashBytes := hash.Sum(nil)
|
||||
|
||||
return hex.EncodeToString(hashBytes)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user