Compare commits

...

6 Commits

11 changed files with 128 additions and 41 deletions

View File

@@ -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,

View File

@@ -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{

View File

@@ -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,

View File

@@ -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" # 数据库文件路径

View File

@@ -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)

View File

@@ -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{

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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,

View File

@@ -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)
}