Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8c348a182 | ||
|
|
725acd0199 | ||
|
|
166c27c70f | ||
|
|
3bdef20e85 | ||
|
|
50fba3f910 | ||
|
|
87d3f14392 | ||
|
|
30452c8d46 | ||
|
|
300f7723af | ||
|
|
491ba55f1e | ||
|
|
32519b8c08 | ||
|
|
7ffd9891a0 |
15
README.md
15
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
# <img src="docs/logo.jpg" width="45" align="center"> Save Any Bot
|
||||
|
||||
**简体中文** | [English](README_EN.md)
|
||||
**简体中文** | [English](README_EN.md)
|
||||
|
||||
把 Telegram 的文件保存到各类存储端.
|
||||
|
||||
@@ -50,7 +50,7 @@ WantedBy=multi-user.target
|
||||
systemctl enable --now saveany-bot
|
||||
```
|
||||
|
||||
#### 为OpenWrt及衍生系统添加开机自启动服务
|
||||
#### 为 OpenWrt 及衍生系统添加开机自启动服务
|
||||
|
||||
创建文件 ` /etc/init.d/saveanybot` ,参考[saveanybot](./docs/saveanybot)自行修改.
|
||||
|
||||
@@ -60,7 +60,7 @@ systemctl enable --now saveany-bot
|
||||
|
||||
`chmod +x /etc/rc.d/S99saveanybot`
|
||||
|
||||
#### 为OpenWrt及衍生系统添加快捷指令
|
||||
#### 为 OpenWrt 及衍生系统添加快捷指令
|
||||
|
||||
创建文件` /usr/bin/sabot` ,参考[sabot](./docs/sabot)自行配置修改,注意此处文件编码仅支持 ANSI 936 .
|
||||
|
||||
@@ -68,7 +68,6 @@ systemctl enable --now saveany-bot
|
||||
|
||||
之后,终端输入`sabot start|stop|restart|status|enable|disable`即可.
|
||||
|
||||
|
||||
### 使用 Docker 部署
|
||||
|
||||
#### Docker Compose
|
||||
@@ -111,6 +110,14 @@ docker restart saveany-bot
|
||||
|
||||
---
|
||||
|
||||
## 赞助
|
||||
|
||||
本项目受到 [YxVM](https://yxvm.com/) 与 [NodeSupport](https://github.com/NodeSeekDev/NodeSupport) 的支持.
|
||||
|
||||
如果这个项目对你有帮助, 你可以考虑通过以下方式赞助我:
|
||||
|
||||
- [爱发电](https://afdian.com/a/acherkrau)
|
||||
|
||||
## Thanks
|
||||
|
||||
- [gotd](https://github.com/gotd/td)
|
||||
|
||||
@@ -92,6 +92,14 @@ 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)
|
||||
|
||||
@@ -27,10 +27,10 @@ func newProxyDialer(proxyUrl string) (proxy.Dialer, error) {
|
||||
}
|
||||
|
||||
func Init() {
|
||||
InitTelegraphClient()
|
||||
common.Log.Info("初始化 Telegram 客户端...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Cfg.Telegram.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
go InitTelegraphClient()
|
||||
resultChan := make(chan struct {
|
||||
client *gotgproto.Client
|
||||
err error
|
||||
@@ -78,6 +78,7 @@ func Init() {
|
||||
{Command: "storage", Description: "设置默认存储端"},
|
||||
{Command: "save", Description: "保存所回复的文件"},
|
||||
{Command: "dir", Description: "管理存储文件夹"},
|
||||
{Command: "rule", Description: "管理规则"},
|
||||
},
|
||||
})
|
||||
resultChan <- struct {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/celestix/gotgproto/dispatcher"
|
||||
@@ -11,51 +13,71 @@ import (
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
)
|
||||
|
||||
func dirCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
args := strings.Split(strings.TrimPrefix(update.EffectiveMessage.Text, "/dir "), " ")
|
||||
if len(args) < 3 {
|
||||
dirs, err := dao.GetUserDirsByChatID(update.GetUserChat().GetID())
|
||||
if err != nil {
|
||||
common.Log.Errorf("获取用户路径失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("获取用户路径失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextStyledTextArray(
|
||||
[]styling.StyledTextOption{
|
||||
styling.Bold("使用方法: /dir <操作> <存储名> <路径>"),
|
||||
styling.Plain("\n\n可用操作:\n"),
|
||||
styling.Code("add"),
|
||||
styling.Plain(" - 添加路径\n"),
|
||||
styling.Code("del"),
|
||||
styling.Plain(" - 删除路径\n"),
|
||||
styling.Plain("\n示例:\n"),
|
||||
styling.Code("/dir add local1 path/to/dir"),
|
||||
styling.Plain("\n\n当前已添加的路径:\n"),
|
||||
styling.Blockquote(func() string {
|
||||
var sb strings.Builder
|
||||
for _, dir := range dirs {
|
||||
sb.WriteString(dir.StorageName)
|
||||
sb.WriteString(" - ")
|
||||
sb.WriteString(dir.Path)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
}(), true),
|
||||
},
|
||||
), nil)
|
||||
func sendDirHelp(ctx *ext.Context, update *ext.Update, userChatID int64) error {
|
||||
dirs, err := dao.GetUserDirsByChatID(userChatID)
|
||||
if err != nil {
|
||||
common.Log.Errorf("获取用户路径失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("获取用户路径失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextStyledTextArray(
|
||||
[]styling.StyledTextOption{
|
||||
styling.Bold("使用方法: /dir <操作> <参数...>"),
|
||||
styling.Plain("\n\n可用操作:\n"),
|
||||
styling.Code("add"),
|
||||
styling.Plain(" <存储名> <路径> - 添加路径\n"),
|
||||
styling.Code("del"),
|
||||
styling.Plain(" <路径ID> - 删除路径\n"),
|
||||
styling.Plain("\n添加路径示例:\n"),
|
||||
styling.Code("/dir add local1 path/to/dir"),
|
||||
styling.Plain("\n\n删除路径示例:\n"),
|
||||
styling.Code("/dir del 3"),
|
||||
styling.Plain("\n\n当前已添加的路径:\n"),
|
||||
styling.Blockquote(func() string {
|
||||
var sb strings.Builder
|
||||
for _, dir := range dirs {
|
||||
sb.WriteString(fmt.Sprintf("%d: ", dir.ID))
|
||||
sb.WriteString(dir.StorageName)
|
||||
sb.WriteString(" - ")
|
||||
sb.WriteString(dir.Path)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
}(), true),
|
||||
},
|
||||
), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
func dirCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||
if len(args) < 2 {
|
||||
return sendDirHelp(ctx, update, update.GetUserChat().GetID())
|
||||
}
|
||||
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||
if err != nil {
|
||||
common.Log.Errorf("获取用户失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
switch args[0] {
|
||||
switch args[1] {
|
||||
case "add":
|
||||
return addDir(ctx, update, user, args[1], args[2])
|
||||
// /dir add local1 path/to/dir
|
||||
if len(args) < 4 {
|
||||
return sendDirHelp(ctx, update, update.GetUserChat().GetID())
|
||||
}
|
||||
return addDir(ctx, update, user, args[2], args[3])
|
||||
case "del":
|
||||
return delDir(ctx, update, user, args[1], args[2])
|
||||
// /dir del 3
|
||||
if len(args) < 3 {
|
||||
return sendDirHelp(ctx, update, update.GetUserChat().GetID())
|
||||
}
|
||||
dirID, err := strconv.Atoi(args[2])
|
||||
if err != nil {
|
||||
ctx.Reply(update, ext.ReplyTextString("路径ID无效"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
return delDir(ctx, update, user, dirID)
|
||||
default:
|
||||
ctx.Reply(update, ext.ReplyTextString("未知操作"), nil)
|
||||
return dispatcher.EndGroups
|
||||
@@ -77,8 +99,8 @@ func addDir(ctx *ext.Context, update *ext.Update, user *dao.User, storageName, p
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
func delDir(ctx *ext.Context, update *ext.Update, user *dao.User, storageName, path string) error {
|
||||
if err := dao.DeleteDirForUser(user.ID, storageName, path); err != nil {
|
||||
func delDir(ctx *ext.Context, update *ext.Update, user *dao.User, dirID int) error {
|
||||
if err := dao.DeleteDirByID(uint(dirID)); err != nil {
|
||||
common.Log.Errorf("删除路径失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("删除路径失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
|
||||
@@ -29,24 +29,40 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
if len(strSlice) < 3 {
|
||||
return dispatcher.ContinueGroups
|
||||
}
|
||||
messageID, err := strconv.Atoi(strSlice[2])
|
||||
messageID, err := strconv.Atoi(strSlice[len(strSlice)-1])
|
||||
if err != nil {
|
||||
common.Log.Errorf("解析消息 ID 失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("无法解析消息 ID"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
chatUsername := strSlice[1]
|
||||
linkChat, err := ctx.ResolveUsername(chatUsername)
|
||||
if err != nil {
|
||||
common.Log.Errorf("解析 Chat ID 失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("无法解析 Chat ID"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if linkChat == nil {
|
||||
common.Log.Errorf("无法找到聊天: %s", chatUsername)
|
||||
ctx.Reply(update, ext.ReplyTextString("无法找到聊天"), nil)
|
||||
var linkChatID int64
|
||||
if len(strSlice) == 3 {
|
||||
chatUsername := strSlice[1]
|
||||
linkChat, err := ctx.ResolveUsername(chatUsername)
|
||||
if err != nil {
|
||||
common.Log.Errorf("解析用户名失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("解析用户名失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if linkChat == nil {
|
||||
common.Log.Errorf("无法找到聊天: %s", chatUsername)
|
||||
ctx.Reply(update, ext.ReplyTextString("无法找到聊天"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
linkChatID = linkChat.GetID()
|
||||
} else if len(strSlice) == 4 {
|
||||
chatID, err := strconv.Atoi(strSlice[2])
|
||||
if err != nil {
|
||||
common.Log.Errorf("解析 Chat ID 失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("解析 Chat ID 失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
linkChatID = int64(chatID)
|
||||
} else {
|
||||
ctx.Reply(update, ext.ReplyTextString("无法解析链接"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||
if err != nil {
|
||||
common.Log.Errorf("获取用户失败: %s", err)
|
||||
@@ -65,7 +81,7 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
file, err := FileFromMessage(ctx, linkChat.GetID(), messageID, "")
|
||||
file, err := FileFromMessage(ctx, linkChatID, messageID, "")
|
||||
if err != nil {
|
||||
common.Log.Errorf("获取文件失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("获取文件失败: "+err.Error()), nil)
|
||||
@@ -78,7 +94,7 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
receivedFile := &dao.ReceivedFile{
|
||||
Processing: false,
|
||||
FileName: file.FileName,
|
||||
ChatID: linkChat.GetID(),
|
||||
ChatID: linkChatID,
|
||||
MessageID: messageID,
|
||||
ReplyMessageID: replied.ID,
|
||||
ReplyChatID: update.GetUserChat().GetID(),
|
||||
@@ -92,7 +108,7 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if !user.Silent || user.DefaultStorage == "" {
|
||||
return ProvideSelectMessage(ctx, update, file.FileName, linkChat.GetID(), messageID, replied.ID)
|
||||
return ProvideSelectMessage(ctx, update, file.FileName, linkChatID, messageID, replied.ID)
|
||||
}
|
||||
return HandleSilentAddTask(ctx, update, user, &types.Task{
|
||||
Ctx: ctx,
|
||||
@@ -100,7 +116,7 @@ func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
|
||||
File: file,
|
||||
StorageName: user.DefaultStorage,
|
||||
UserID: user.ChatID,
|
||||
FileChatID: linkChat.GetID(),
|
||||
FileChatID: linkChatID,
|
||||
FileMessageID: messageID,
|
||||
ReplyMessageID: replied.ID,
|
||||
ReplyChatID: update.GetUserChat().GetID(),
|
||||
|
||||
141
bot/handle_rule.go
Normal file
141
bot/handle_rule.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/celestix/gotgproto/dispatcher"
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/gotd/td/telegram/message/styling"
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/krau/SaveAny-Bot/dao"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
func sendRuleHelp(ctx *ext.Context, update *ext.Update, userChatID int64) error {
|
||||
user, err := dao.GetUserByChatID(userChatID)
|
||||
if err != nil {
|
||||
common.Log.Errorf("获取用户规则失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("获取用户规则失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextStyledTextArray(
|
||||
[]styling.StyledTextOption{
|
||||
styling.Bold("使用方法: /rule <操作> <参数...>"),
|
||||
styling.Bold(fmt.Sprintf("\n当前已%s规则模式", map[bool]string{true: "启用", false: "禁用"}[user.ApplyRule])),
|
||||
styling.Plain("\n\n可用操作:\n"),
|
||||
styling.Code("switch"),
|
||||
styling.Plain(" - 开关规则模式\n"),
|
||||
styling.Code("add"),
|
||||
styling.Plain(" <类型> <数据> <存储名> <路径> - 添加规则\n"),
|
||||
styling.Code("del"),
|
||||
styling.Plain(" <规则ID> - 删除规则\n"),
|
||||
styling.Plain("\n当前已添加的规则:\n"),
|
||||
styling.Blockquote(func() string {
|
||||
var sb strings.Builder
|
||||
for _, rule := range user.Rules {
|
||||
ruleText := fmt.Sprintf("%s %s %s %s", rule.Type, rule.Data, rule.StorageName, rule.DirPath)
|
||||
sb.WriteString(fmt.Sprintf("%d: %s\n", rule.ID, ruleText))
|
||||
}
|
||||
return sb.String()
|
||||
}(), true),
|
||||
},
|
||||
), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
func ruleCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||
if len(args) < 2 {
|
||||
return sendRuleHelp(ctx, update, update.GetUserChat().GetID())
|
||||
}
|
||||
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||
if err != nil {
|
||||
common.Log.Errorf("获取用户失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
switch args[1] {
|
||||
case "switch":
|
||||
// /rule switch
|
||||
return switchApplyRule(ctx, update, user)
|
||||
case "add":
|
||||
// /rule add <type> <data> <storage> <dirpath>
|
||||
if len(args) < 6 {
|
||||
return sendRuleHelp(ctx, update, user.ChatID)
|
||||
}
|
||||
return addRule(ctx, update, user, args)
|
||||
case "del":
|
||||
// /rule del <id>
|
||||
if len(args) < 3 {
|
||||
return sendRuleHelp(ctx, update, user.ChatID)
|
||||
}
|
||||
ruleID := args[2]
|
||||
id, err := strconv.Atoi(ruleID)
|
||||
if err != nil {
|
||||
ctx.Reply(update, ext.ReplyTextString("无效的规则ID"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if err := dao.DeleteRule(uint(id)); err != nil {
|
||||
common.Log.Errorf("删除规则失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("删除规则失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextString("删除规则成功"), nil)
|
||||
return dispatcher.EndGroups
|
||||
default:
|
||||
return sendRuleHelp(ctx, update, user.ChatID)
|
||||
}
|
||||
}
|
||||
|
||||
func switchApplyRule(ctx *ext.Context, update *ext.Update, user *dao.User) error {
|
||||
applyRule := !user.ApplyRule
|
||||
if err := dao.UpdateUserApplyRule(user.ChatID, applyRule); err != nil {
|
||||
common.Log.Errorf("更新用户失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("更新用户失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if applyRule {
|
||||
ctx.Reply(update, ext.ReplyTextString("已启用规则模式"), nil)
|
||||
} else {
|
||||
ctx.Reply(update, ext.ReplyTextString("已禁用规则模式"), nil)
|
||||
}
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
func addRule(ctx *ext.Context, update *ext.Update, user *dao.User, args []string) error {
|
||||
// /rule add <type> <data> <storage> <dirpath>
|
||||
ruleType := args[2]
|
||||
ruleData := args[3]
|
||||
storageName := args[4]
|
||||
dirPath := args[5]
|
||||
|
||||
if !slice.Contain(types.RuleTypes, types.RuleType(ruleType)) {
|
||||
var ruleTypesStylingArray []styling.StyledTextOption
|
||||
ruleTypesStylingArray = append(ruleTypesStylingArray, styling.Bold("无效的规则类型, 可用类型:\n"))
|
||||
for i, ruleType := range types.RuleTypes {
|
||||
ruleTypesStylingArray = append(ruleTypesStylingArray, styling.Code(string(ruleType)))
|
||||
if i != len(types.RuleTypes)-1 {
|
||||
ruleTypesStylingArray = append(ruleTypesStylingArray, styling.Plain(", "))
|
||||
}
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextStyledTextArray(ruleTypesStylingArray), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
rule := &dao.Rule{
|
||||
Type: ruleType,
|
||||
Data: ruleData,
|
||||
StorageName: storageName,
|
||||
DirPath: dirPath,
|
||||
UserID: user.ID,
|
||||
}
|
||||
if err := dao.CreateRule(rule); err != nil {
|
||||
common.Log.Errorf("添加规则失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("添加规则失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextString("添加规则成功"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/celestix/gotgproto/dispatcher"
|
||||
@@ -9,25 +10,50 @@ import (
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/krau/SaveAny-Bot/dao"
|
||||
"github.com/krau/SaveAny-Bot/queue"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
func sendSaveHelp(ctx *ext.Context, update *ext.Update) error {
|
||||
helpText := `
|
||||
使用方法:
|
||||
|
||||
1. 使用该命令回复要保存的文件, 可选文件名参数.
|
||||
示例:
|
||||
/save custom_file_name.mp4
|
||||
|
||||
2. 设置默认存储后, 发送 /save <频道ID/用户名> <消息ID范围> 来批量保存文件. 遵从存储规则, 若未匹配到任何规则则使用默认存储.
|
||||
示例:
|
||||
/save @moreacg 114-514
|
||||
`
|
||||
ctx.Reply(update, ext.ReplyTextString(helpText), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
res, ok := update.EffectiveMessage.GetReplyTo()
|
||||
if !ok || res == nil {
|
||||
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
||||
return dispatcher.EndGroups
|
||||
args := strings.Split(update.EffectiveMessage.Text, " ")
|
||||
if len(args) >= 3 {
|
||||
return handleBatchSave(ctx, update, args[1:])
|
||||
}
|
||||
replyHeader, ok := res.(*tg.MessageReplyHeader)
|
||||
if !ok {
|
||||
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
replyToMsgID, ok := replyHeader.GetReplyToMsgID()
|
||||
if !ok {
|
||||
ctx.Reply(update, ext.ReplyTextString("请回复要保存的文件"), nil)
|
||||
return dispatcher.EndGroups
|
||||
|
||||
replyToMsgID := func() int {
|
||||
res, ok := update.EffectiveMessage.GetReplyTo()
|
||||
if !ok || res == nil {
|
||||
return 0
|
||||
}
|
||||
replyHeader, ok := res.(*tg.MessageReplyHeader)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
replyToMsgID, ok := replyHeader.GetReplyToMsgID()
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return replyToMsgID
|
||||
}()
|
||||
if replyToMsgID == 0 {
|
||||
return sendSaveHelp(ctx, update)
|
||||
}
|
||||
|
||||
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||
@@ -113,3 +139,125 @@ func saveCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
UserID: user.ChatID,
|
||||
})
|
||||
}
|
||||
|
||||
func handleBatchSave(ctx *ext.Context, update *ext.Update, args []string) error {
|
||||
// args: [0] = @channel, [1] = 114-514
|
||||
chatArg := args[0]
|
||||
var chatID int64
|
||||
var err error
|
||||
msgIdSlice := strings.Split(args[1], "-")
|
||||
if len(msgIdSlice) != 2 {
|
||||
ctx.Reply(update, ext.ReplyTextString("无效的消息ID范围"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
minMsgID, minerr := strconv.ParseInt(msgIdSlice[0], 10, 64)
|
||||
maxMsgID, maxerr := strconv.ParseInt(msgIdSlice[1], 10, 64)
|
||||
if minerr != nil || maxerr != nil {
|
||||
ctx.Reply(update, ext.ReplyTextString("无效的消息ID范围"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if minMsgID > maxMsgID || minMsgID <= 0 || maxMsgID <= 0 {
|
||||
ctx.Reply(update, ext.ReplyTextString("无效的消息ID范围"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
|
||||
if err != nil {
|
||||
common.Log.Errorf("获取用户失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if user.DefaultStorage == "" {
|
||||
ctx.Reply(update, ext.ReplyTextString("请先设置默认存储"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
storages := storage.GetUserStorages(user.ChatID)
|
||||
if len(storages) == 0 {
|
||||
ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
if strings.HasPrefix(chatArg, "@") {
|
||||
chatUsername := strings.TrimPrefix(chatArg, "@")
|
||||
chat, err := ctx.ResolveUsername(chatUsername)
|
||||
if err != nil {
|
||||
common.Log.Errorf("解析频道用户名失败: %s", err)
|
||||
ctx.Reply(update, ext.ReplyTextString("解析频道用户名失败"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
if chat == nil {
|
||||
ctx.Reply(update, ext.ReplyTextString("无法找到聊天"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
chatID = chat.GetID()
|
||||
} else {
|
||||
chatID, err = strconv.ParseInt(chatArg, 10, 64)
|
||||
if err != nil {
|
||||
ctx.Reply(update, ext.ReplyTextString("无效的频道ID或用户名"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
}
|
||||
if chatID == 0 {
|
||||
ctx.Reply(update, ext.ReplyTextString("无效的频道ID或用户名"), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
replied, err := ctx.Reply(update, ext.ReplyTextString("正在批量保存..."), nil)
|
||||
if err != nil {
|
||||
common.Log.Errorf("回复失败: %s", err)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
total := maxMsgID - minMsgID + 1
|
||||
successadd := 0
|
||||
failedGetFile := 0
|
||||
failedGetMsg := 0
|
||||
failedSaveDB := 0
|
||||
for i := minMsgID; i <= maxMsgID; i++ {
|
||||
file, err := FileFromMessage(ctx, chatID, int(i), "")
|
||||
if err != nil {
|
||||
common.Log.Errorf("获取文件失败: %s", err)
|
||||
failedGetFile++
|
||||
continue
|
||||
}
|
||||
if file.FileName == "" {
|
||||
message, err := GetTGMessage(ctx, chatID, int(i))
|
||||
if err != nil {
|
||||
common.Log.Errorf("获取消息失败: %s", err)
|
||||
failedGetMsg++
|
||||
continue
|
||||
}
|
||||
file.FileName = GenFileNameFromMessage(*message, file)
|
||||
}
|
||||
receivedFile := &dao.ReceivedFile{
|
||||
Processing: false,
|
||||
FileName: file.FileName,
|
||||
ChatID: chatID,
|
||||
MessageID: int(i),
|
||||
ReplyChatID: update.GetUserChat().GetID(),
|
||||
ReplyMessageID: 0,
|
||||
}
|
||||
if err := dao.SaveReceivedFile(receivedFile); err != nil {
|
||||
common.Log.Errorf("保存接收的文件失败: %s", err)
|
||||
failedSaveDB++
|
||||
continue
|
||||
}
|
||||
task := &types.Task{
|
||||
Ctx: ctx,
|
||||
Status: types.Pending,
|
||||
File: file,
|
||||
StorageName: user.DefaultStorage,
|
||||
FileChatID: chatID,
|
||||
FileMessageID: int(i),
|
||||
UserID: user.ChatID,
|
||||
ReplyMessageID: 0,
|
||||
ReplyChatID: update.GetUserChat().GetID(),
|
||||
}
|
||||
queue.AddTask(task)
|
||||
successadd++
|
||||
}
|
||||
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("批量保存完成\n成功添加: %d/%d\n获取文件失败: %d\n获取消息失败: %d\n保存数据库失败: %d", successadd, total, failedGetFile, failedGetMsg, failedSaveDB),
|
||||
ID: replied.ID,
|
||||
})
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
|
||||
dispatcher.AddHandler(handlers.NewCommand("storage", storageCmd))
|
||||
dispatcher.AddHandler(handlers.NewCommand("save", saveCmd))
|
||||
dispatcher.AddHandler(handlers.NewCommand("dir", dirCmd))
|
||||
dispatcher.AddHandler(handlers.NewCommand("rule", ruleCmd))
|
||||
linkRegexFilter, err := filters.Message.Regex(linkRegexString)
|
||||
if err != nil {
|
||||
common.Log.Panicf("创建正则表达式过滤器失败: %s", err)
|
||||
|
||||
17
bot/utils.go
17
bot/utils.go
@@ -180,12 +180,11 @@ func FileFromMedia(media tg.MessageMediaClass, customFileName string) (*types.Fi
|
||||
|
||||
func FileFromMessage(ctx *ext.Context, chatID int64, messageID int, customFileName string) (*types.File, error) {
|
||||
key := fmt.Sprintf("file:%d:%d", chatID, messageID)
|
||||
common.Log.Debugf("Getting file: %s", key)
|
||||
var cachedFile types.File
|
||||
err := common.Cache.Get(key, &cachedFile)
|
||||
cachedFile, err := common.CacheGet[*types.File](ctx, key)
|
||||
if err == nil {
|
||||
return &cachedFile, nil
|
||||
return cachedFile, nil
|
||||
}
|
||||
common.Log.Debugf("Getting file: %s", key)
|
||||
message, err := GetTGMessage(ctx, chatID, messageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -194,13 +193,18 @@ func FileFromMessage(ctx *ext.Context, chatID int64, messageID int, customFileNa
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := common.Cache.Set(key, file, 3600); err != nil {
|
||||
if err := common.CacheSet(ctx, key, file); err != nil {
|
||||
common.Log.Errorf("Failed to cache file: %s", err)
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func GetTGMessage(ctx *ext.Context, chatId int64, messageID int) (*tg.Message, error) {
|
||||
key := fmt.Sprintf("message:%d:%d", chatId, messageID)
|
||||
cacheMessage, err := common.CacheGet[*tg.Message](ctx, key)
|
||||
if err == nil {
|
||||
return cacheMessage, nil
|
||||
}
|
||||
common.Log.Debugf("Fetching message: %d", messageID)
|
||||
messages, err := ctx.GetMessages(chatId, []tg.InputMessageClass{&tg.InputMessageID{ID: messageID}})
|
||||
if err != nil {
|
||||
@@ -214,6 +218,9 @@ func GetTGMessage(ctx *ext.Context, chatId int64, messageID int) (*tg.Message, e
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected message type: %T", msg)
|
||||
}
|
||||
if err := common.CacheSet(ctx, key, tgMessage); err != nil {
|
||||
common.Log.Errorf("Failed to cache message: %s", err)
|
||||
}
|
||||
return tgMessage, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +1,38 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"sync"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/coocood/freecache"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
"github.com/eko/gocache/lib/v4/cache"
|
||||
gocachestore "github.com/eko/gocache/store/go_cache/v4"
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type CommonCache struct {
|
||||
cache *freecache.Cache
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var Cache *CommonCache
|
||||
var Cache *cache.Cache[any]
|
||||
|
||||
func initCache() {
|
||||
gob.Register(types.File{})
|
||||
gob.Register(tg.InputDocumentFileLocation{})
|
||||
gob.Register(tg.InputPhotoFileLocation{})
|
||||
Cache = &CommonCache{cache: freecache.NewCache(10 * 1024 * 1024)}
|
||||
gocacheClient := gocache.New(time.Hour*1, time.Minute*10)
|
||||
gocacheStore := gocachestore.NewGoCache(gocacheClient)
|
||||
cacheManager := cache.New[any](gocacheStore)
|
||||
Cache = cacheManager
|
||||
}
|
||||
|
||||
func (c *CommonCache) Get(key string, value any) error {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
data, err := Cache.cache.Get([]byte(key))
|
||||
func CacheGet[T any](ctx context.Context, key string) (T, error) {
|
||||
data, err := Cache.Get(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
return *new(T), err
|
||||
}
|
||||
dec := gob.NewDecoder(bytes.NewReader(data))
|
||||
err = dec.Decode(&value)
|
||||
if err != nil {
|
||||
return err
|
||||
if v, ok := data.(T); ok {
|
||||
return v, nil
|
||||
}
|
||||
return nil
|
||||
return *new(T), nil
|
||||
}
|
||||
|
||||
func (c *CommonCache) Set(key string, value any, expireSeconds int) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
err := enc.Encode(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Cache.cache.Set([]byte(key), buf.Bytes(), expireSeconds)
|
||||
return nil
|
||||
func CacheSet(ctx context.Context, key string, value any) error {
|
||||
return Cache.Set(ctx, key, value)
|
||||
}
|
||||
|
||||
func (c *CommonCache) Delete(key string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
Cache.cache.Del([]byte(key))
|
||||
return nil
|
||||
func CacheDelete(ctx context.Context, key string) error {
|
||||
return Cache.Delete(ctx, key)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ token = ""
|
||||
# app_id = 123456
|
||||
# app_hash = "0123456789abcdef0123456789abcdef"
|
||||
|
||||
# 初始化超时时间, 单位: 秒
|
||||
timeout = 60
|
||||
|
||||
[telegram.proxy]
|
||||
# 启用代理连接 telegram, 只支持 socks5
|
||||
enable = false
|
||||
@@ -30,12 +33,6 @@ enable = true
|
||||
# 文件保存根路径
|
||||
base_path = "./downloads"
|
||||
|
||||
[[storages]]
|
||||
name = "本机2"
|
||||
type = "local"
|
||||
enable = true
|
||||
base_path = "./downloads/2"
|
||||
|
||||
[[storages]]
|
||||
name = "MyAlist"
|
||||
type = "alist"
|
||||
@@ -49,7 +46,6 @@ token_exp = 86400 # 86400--1天 604800--7天 1296000--15天 2592000--30
|
||||
# 请自行在 alist 侧配置合理的 token 过期时间
|
||||
# token = ""
|
||||
|
||||
|
||||
[[storages]]
|
||||
name = "MyWebdav"
|
||||
type = "webdav"
|
||||
|
||||
@@ -37,13 +37,15 @@ type logConfig struct {
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
Path string `toml:"path" mapstructure:"path"`
|
||||
Path string `toml:"path" mapstructure:"path"`
|
||||
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"`
|
||||
|
||||
// Deprecated
|
||||
@@ -82,6 +84,7 @@ func Init() error {
|
||||
|
||||
viper.SetDefault("telegram.app_id", 1025907)
|
||||
viper.SetDefault("telegram.app_hash", "452b0359b988148995f22ff0f4229750")
|
||||
viper.SetDefault("telegram.timeout", 60)
|
||||
|
||||
viper.SetDefault("temp.base_path", "cache/")
|
||||
viper.SetDefault("temp.cache_ttl", 3600)
|
||||
@@ -91,6 +94,7 @@ func Init() error {
|
||||
viper.SetDefault("log.backup_count", 7)
|
||||
|
||||
viper.SetDefault("db.path", "data/saveany.db")
|
||||
viper.SetDefault("db.expire", 86400*5)
|
||||
|
||||
if err := viper.SafeWriteConfigAs("config.toml"); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileAlreadyExistsError); !ok {
|
||||
|
||||
@@ -46,7 +46,7 @@ func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
||||
extCtx, ok := task.Ctx.(*ext.Context)
|
||||
if !ok {
|
||||
common.Log.Errorf("Context is not *ext.Context: %T", task.Ctx)
|
||||
} else {
|
||||
} else if task.ReplyMessageID != 0 {
|
||||
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("文件保存成功\n [%s]: %s", task.StorageName, task.StoragePath),
|
||||
ID: task.ReplyMessageID,
|
||||
@@ -57,7 +57,7 @@ func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
||||
extCtx, ok := task.Ctx.(*ext.Context)
|
||||
if !ok {
|
||||
common.Log.Errorf("Context is not *ext.Context: %T", task.Ctx)
|
||||
} else {
|
||||
} else if task.ReplyMessageID != 0 {
|
||||
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: "文件保存失败\n" + task.Error.Error(),
|
||||
ID: task.ReplyMessageID,
|
||||
@@ -68,7 +68,7 @@ func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
||||
extCtx, ok := task.Ctx.(*ext.Context)
|
||||
if !ok {
|
||||
common.Log.Errorf("Context is not *ext.Context: %T", task.Ctx)
|
||||
} else {
|
||||
} else if task.ReplyMessageID != 0 {
|
||||
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: "任务已取消",
|
||||
ID: task.ReplyMessageID,
|
||||
|
||||
154
core/download.go
154
core/download.go
@@ -31,15 +31,14 @@ func processPendingTask(task *types.Task) error {
|
||||
task.File.FileName = fmt.Sprintf("%d_%d_%s", task.FileChatID, task.FileMessageID, task.File.Hash())
|
||||
}
|
||||
|
||||
if task.StoragePath == "" {
|
||||
task.StoragePath = task.FileName()
|
||||
}
|
||||
|
||||
taskStorage, err := storage.GetStorageByUserIDAndName(task.UserID, task.StorageName)
|
||||
taskStorage, storagePath, err := getStorageAndPathForTask(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task.StoragePath = taskStorage.JoinStoragePath(*task)
|
||||
if taskStorage == nil {
|
||||
return fmt.Errorf("not found storage: %s", task.StorageName)
|
||||
}
|
||||
task.StoragePath = storagePath
|
||||
|
||||
ctx, ok := task.Ctx.(*ext.Context)
|
||||
if !ok {
|
||||
@@ -59,41 +58,55 @@ func processPendingTask(task *types.Task) error {
|
||||
|
||||
downloadBuilder := Downloader.Download(bot.Client.API(), task.File.Location).WithThreads(getTaskThreads(task.File.FileSize))
|
||||
|
||||
notsupportStreamStorage, notsupportStream := taskStorage.(storage.StorageNotSupportStream)
|
||||
cancelMarkUp := getCancelTaskMarkup(task)
|
||||
if config.Cfg.Stream {
|
||||
|
||||
text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0)
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
Entities: entities,
|
||||
ID: task.ReplyMessageID,
|
||||
ReplyMarkup: getCancelTaskMarkup(task),
|
||||
})
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
defer pr.Close()
|
||||
|
||||
task.StartTime = time.Now()
|
||||
progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize))
|
||||
|
||||
progressStream := NewProgressStream(pw, task.File.FileSize, progressCallback)
|
||||
|
||||
eg, uploadCtx := errgroup.WithContext(cancelCtx)
|
||||
|
||||
eg.Go(func() error {
|
||||
return taskStorage.Save(uploadCtx, pr, task.StoragePath)
|
||||
})
|
||||
eg.Go(func() error {
|
||||
_, err := downloadBuilder.Stream(uploadCtx, progressStream)
|
||||
if closeErr := pw.CloseWithError(err); closeErr != nil {
|
||||
common.Log.Errorf("Failed to close pipe writer: %v", closeErr)
|
||||
if !notsupportStream {
|
||||
text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0)
|
||||
if task.ReplyMessageID != 0 {
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
Entities: entities,
|
||||
ID: task.ReplyMessageID,
|
||||
ReplyMarkup: cancelMarkUp,
|
||||
})
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err := eg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
pr, pw := io.Pipe()
|
||||
defer pr.Close()
|
||||
|
||||
task.StartTime = time.Now()
|
||||
progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize))
|
||||
|
||||
progressStream := NewProgressStream(pw, task.File.FileSize, progressCallback)
|
||||
|
||||
eg, uploadCtx := errgroup.WithContext(cancelCtx)
|
||||
|
||||
eg.Go(func() error {
|
||||
return taskStorage.Save(uploadCtx, pr, task.StoragePath)
|
||||
})
|
||||
eg.Go(func() error {
|
||||
_, err := downloadBuilder.Stream(uploadCtx, progressStream)
|
||||
if closeErr := pw.CloseWithError(err); closeErr != nil {
|
||||
common.Log.Errorf("Failed to close pipe writer: %v", closeErr)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err := eg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
common.Log.Warnf("存储 %s 不支持流式传输: %s", task.StorageName, notsupportStreamStorage.NotSupportStream())
|
||||
|
||||
if task.ReplyMessageID != 0 {
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("存储 %s 不支持流式传输: %s\n正在使用普通下载...", task.StorageName, notsupportStreamStorage.NotSupportStream()),
|
||||
ID: task.ReplyMessageID,
|
||||
ReplyMarkup: cancelMarkUp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
cacheDestPath := filepath.Join(config.Cfg.Temp.BasePath, task.FileName())
|
||||
@@ -106,12 +119,14 @@ func processPendingTask(task *types.Task) error {
|
||||
}
|
||||
|
||||
text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0)
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
Entities: entities,
|
||||
ID: task.ReplyMessageID,
|
||||
ReplyMarkup: getCancelTaskMarkup(task),
|
||||
})
|
||||
if task.ReplyMessageID != 0 {
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
Entities: entities,
|
||||
ID: task.ReplyMessageID,
|
||||
ReplyMarkup: cancelMarkUp,
|
||||
})
|
||||
}
|
||||
|
||||
progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize))
|
||||
dest, err := NewTaskLocalFile(cacheDestPath, task.File.FileSize, progressCallback)
|
||||
@@ -129,11 +144,12 @@ func processPendingTask(task *types.Task) error {
|
||||
fixTaskFileExt(task, cacheDestPath)
|
||||
|
||||
common.Log.Infof("Downloaded file: %s", cacheDestPath)
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()),
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
|
||||
if task.ReplyMessageID != 0 {
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: fmt.Sprintf("下载完成: %s\n正在转存文件...", task.FileName()),
|
||||
ID: task.ReplyMessageID,
|
||||
})
|
||||
}
|
||||
return saveFileWithRetry(cancelCtx, task.StoragePath, taskStorage, cacheDestPath)
|
||||
}
|
||||
|
||||
@@ -161,12 +177,14 @@ func processTelegraph(extCtx *ext.Context, cancelCtx context.Context, task *type
|
||||
common.Log.Errorf("Failed to build entities: %s", err)
|
||||
}
|
||||
|
||||
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
Entities: entities,
|
||||
ID: task.ReplyMessageID,
|
||||
ReplyMarkup: getCancelTaskMarkup(task),
|
||||
})
|
||||
if task.ReplyMessageID != 0 {
|
||||
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
Entities: entities,
|
||||
ID: task.ReplyMessageID,
|
||||
ReplyMarkup: getCancelTaskMarkup(task),
|
||||
})
|
||||
}
|
||||
|
||||
resultCh := make(chan error)
|
||||
go func() {
|
||||
@@ -191,7 +209,7 @@ func processTelegraph(extCtx *ext.Context, cancelCtx context.Context, task *type
|
||||
|
||||
if len(node.Children) != 0 {
|
||||
for _, child := range node.Children {
|
||||
imgs = append(imgs, GetImages(child)...)
|
||||
imgs = append(imgs, getNodeImages(child)...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,27 +283,3 @@ func processTelegraph(extCtx *ext.Context, cancelCtx context.Context, task *type
|
||||
return cancelCtx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func GetImages(node telegraph.Node) []string {
|
||||
var srcs []string
|
||||
|
||||
var nodeElement telegraph.NodeElement
|
||||
data, err := json.Marshal(node)
|
||||
if err != nil {
|
||||
return srcs
|
||||
}
|
||||
err = json.Unmarshal(data, &nodeElement)
|
||||
if err != nil {
|
||||
return srcs
|
||||
}
|
||||
|
||||
if nodeElement.Tag == "img" {
|
||||
if src, exists := nodeElement.Attrs["src"]; exists {
|
||||
srcs = append(srcs, src)
|
||||
}
|
||||
}
|
||||
for _, child := range nodeElement.Children {
|
||||
srcs = append(srcs, GetImages(child)...)
|
||||
}
|
||||
return srcs
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestGetImgSrcs(t *testing.T) {
|
||||
"https://example.com/image4.png",
|
||||
}
|
||||
|
||||
got := GetImages(complexStructure)
|
||||
got := getNodeImages(complexStructure)
|
||||
|
||||
if !reflect.DeepEqual(expected, got) {
|
||||
t.Errorf("expected %v,got %v", expected, got)
|
||||
|
||||
103
core/rule.go
Normal file
103
core/rule.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/krau/SaveAny-Bot/bot"
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/krau/SaveAny-Bot/dao"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
func getStorageAndPathForTask(task *types.Task) (storage.Storage, string, error) {
|
||||
user, err := dao.GetUserByChatID(task.UserID)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to get user by chat ID: %w", err)
|
||||
}
|
||||
if task.StoragePath == "" {
|
||||
task.StoragePath = task.FileName()
|
||||
}
|
||||
taskStorage, err := storage.GetStorageByUserIDAndName(task.UserID, task.StorageName)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
storagePath := taskStorage.JoinStoragePath(*task)
|
||||
if !user.ApplyRule || user.Rules == nil {
|
||||
return taskStorage, storagePath, nil
|
||||
}
|
||||
var ruleTaskStorage storage.Storage
|
||||
var ruleStoragePath string
|
||||
for _, rule := range user.Rules {
|
||||
matchStorage, matchStoragePath := applyRule(&rule, *task)
|
||||
if matchStorage != nil && matchStoragePath != "" {
|
||||
ruleTaskStorage = matchStorage
|
||||
ruleStoragePath = matchStoragePath
|
||||
}
|
||||
}
|
||||
if ruleStoragePath == "" || ruleTaskStorage == nil {
|
||||
return taskStorage, storagePath, nil
|
||||
}
|
||||
common.Log.Debugf("Rule matched: %s, %s", ruleTaskStorage.Name(), ruleStoragePath)
|
||||
return ruleTaskStorage, ruleStoragePath, nil
|
||||
}
|
||||
|
||||
func applyRule(rule *dao.Rule, task types.Task) (storage.Storage, string) {
|
||||
var DirPath, StorageName string
|
||||
switch rule.Type {
|
||||
case string(types.RuleTypeFileNameRegex):
|
||||
ruleRegex, err := regexp.Compile(rule.Data)
|
||||
if err != nil {
|
||||
common.Log.Errorf("failed to compile regex: %s", err)
|
||||
return nil, ""
|
||||
}
|
||||
if !ruleRegex.MatchString(task.FileName()) {
|
||||
return nil, ""
|
||||
}
|
||||
DirPath = rule.DirPath
|
||||
StorageName = rule.StorageName
|
||||
case string(types.RuleTypeMessageRegex):
|
||||
ruleRegex, err := regexp.Compile(rule.Data)
|
||||
if err != nil {
|
||||
common.Log.Errorf("failed to compile regex: %s", err)
|
||||
return nil, ""
|
||||
}
|
||||
ctx, ok := task.Ctx.(*ext.Context)
|
||||
if !ok {
|
||||
common.Log.Fatalf("context is not *ext.Context: %T", task.Ctx)
|
||||
return nil, ""
|
||||
}
|
||||
msg, err := bot.GetTGMessage(ctx, task.FileChatID, task.FileMessageID)
|
||||
if err != nil {
|
||||
common.Log.Errorf("failed to get message: %s", err)
|
||||
return nil, ""
|
||||
}
|
||||
if msg == nil {
|
||||
return nil, ""
|
||||
}
|
||||
if !ruleRegex.MatchString(msg.GetMessage()) {
|
||||
return nil, ""
|
||||
}
|
||||
DirPath = rule.DirPath
|
||||
StorageName = rule.StorageName
|
||||
default:
|
||||
common.Log.Errorf("unknown rule type: %s", rule.Type)
|
||||
return nil, ""
|
||||
}
|
||||
taskStorageName := func() string {
|
||||
if StorageName == "" || StorageName == "CHOSEN" {
|
||||
return task.StorageName
|
||||
}
|
||||
return StorageName
|
||||
}()
|
||||
taskStorage, err := storage.GetStorageByUserIDAndName(task.UserID, taskStorageName)
|
||||
if err != nil {
|
||||
common.Log.Errorf("failed to get storage: %s", err)
|
||||
return nil, ""
|
||||
}
|
||||
task.StoragePath = path.Join(DirPath, task.StoragePath)
|
||||
return taskStorage, taskStorage.JoinStoragePath(task)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/celestix/telegraph-go/v2"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gotd/td/telegram/message/entity"
|
||||
"github.com/gotd/td/telegram/message/styling"
|
||||
@@ -22,22 +24,33 @@ import (
|
||||
)
|
||||
|
||||
func saveFileWithRetry(ctx context.Context, storagePath string, taskStorage storage.Storage, cacheFilePath string) error {
|
||||
file, err := os.Open(cacheFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open cache file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
fileStat, err := file.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file stat: %w", err)
|
||||
}
|
||||
vctx := context.WithValue(ctx, types.ContextKeyContentLength, fileStat.Size())
|
||||
for i := 0; i <= config.Cfg.Retry; i++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
if err := vctx.Err(); err != nil {
|
||||
return fmt.Errorf("context canceled while saving file: %w", err)
|
||||
}
|
||||
file, err := os.Open(cacheFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open cache file: %w", err)
|
||||
}
|
||||
if err := taskStorage.Save(ctx, file, storagePath); err != nil {
|
||||
defer file.Close()
|
||||
if err := taskStorage.Save(vctx, file, storagePath); err != nil {
|
||||
if i == config.Cfg.Retry {
|
||||
return fmt.Errorf("failed to save file: %w", err)
|
||||
}
|
||||
common.Log.Errorf("Failed to save file: %s, retrying...", err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("context canceled during retry delay: %w", ctx.Err())
|
||||
case <-vctx.Done():
|
||||
return fmt.Errorf("context canceled during retry delay: %w", vctx.Err())
|
||||
case <-time.After(time.Duration(i*500) * time.Millisecond):
|
||||
}
|
||||
continue
|
||||
@@ -132,6 +145,9 @@ func buildProgressCallback(ctx *ext.Context, task *types.Task, updateCount int)
|
||||
if task.File.FileSize < 1024*1024*50 || progressInt == 0 || progressInt%int(100/updateCount) != 0 {
|
||||
return
|
||||
}
|
||||
if task.ReplyMessageID == 0 {
|
||||
return
|
||||
}
|
||||
text, entities := buildProgressMessageEntity(task, bytesRead, task.StartTime, progress)
|
||||
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
|
||||
Message: text,
|
||||
@@ -256,3 +272,27 @@ func NewProgressStream(writer io.Writer, size int64, callback func(bytesRead, co
|
||||
interval: interval,
|
||||
}
|
||||
}
|
||||
|
||||
func getNodeImages(node telegraph.Node) []string {
|
||||
var srcs []string
|
||||
|
||||
var nodeElement telegraph.NodeElement
|
||||
data, err := json.Marshal(node)
|
||||
if err != nil {
|
||||
return srcs
|
||||
}
|
||||
err = json.Unmarshal(data, &nodeElement)
|
||||
if err != nil {
|
||||
return srcs
|
||||
}
|
||||
|
||||
if nodeElement.Tag == "img" {
|
||||
if src, exists := nodeElement.Attrs["src"]; exists {
|
||||
srcs = append(srcs, src)
|
||||
}
|
||||
}
|
||||
for _, child := range nodeElement.Children {
|
||||
srcs = append(srcs, getNodeImages(child)...)
|
||||
}
|
||||
return srcs
|
||||
}
|
||||
|
||||
48
dao/db.go
48
dao/db.go
@@ -1,6 +1,7 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -17,8 +18,7 @@ var db *gorm.DB
|
||||
|
||||
func Init() {
|
||||
if err := os.MkdirAll(filepath.Dir(config.Cfg.DB.Path), 0755); err != nil {
|
||||
common.Log.Fatal("Failed to create data directory: ", err)
|
||||
os.Exit(1)
|
||||
common.Log.Panic("Failed to create data directory: ", err)
|
||||
}
|
||||
var err error
|
||||
db, err = gorm.Open(sqlite.Open(config.Cfg.DB.Path), &gorm.Config{
|
||||
@@ -32,17 +32,25 @@ func Init() {
|
||||
PrepareStmt: true,
|
||||
})
|
||||
if err != nil {
|
||||
common.Log.Fatal("Failed to open database: ", err)
|
||||
os.Exit(1)
|
||||
common.Log.Panic("Failed to open database: ", err)
|
||||
}
|
||||
common.Log.Debug("Database connected")
|
||||
if err := db.AutoMigrate(&ReceivedFile{}, &User{}, &Dir{}, &CallbackData{}); err != nil {
|
||||
common.Log.Fatal("迁移数据库失败, 如果您从旧版本升级, 建议手动删除数据库文件后重试: ", err)
|
||||
if err := db.AutoMigrate(&ReceivedFile{}, &User{}, &Dir{}, &CallbackData{}, &Rule{}); err != nil {
|
||||
common.Log.Panic("迁移数据库失败, 如果您从旧版本升级, 建议手动删除数据库文件后重试: ", err)
|
||||
}
|
||||
|
||||
if err := syncUsers(); err != nil {
|
||||
common.Log.Fatal("Failed to sync users:", err)
|
||||
common.Log.Panic("Failed to sync users:", err)
|
||||
}
|
||||
common.Log.Debug("Database migrated")
|
||||
if config.Cfg.DB.Expire == 0 {
|
||||
return
|
||||
}
|
||||
if err := cleanExpiredData(db); err != nil {
|
||||
common.Log.Error("Failed to clean expired data: ", err)
|
||||
} else {
|
||||
common.Log.Debug("Cleaned expired data")
|
||||
}
|
||||
go cleanJob(db)
|
||||
}
|
||||
|
||||
func syncUsers() error {
|
||||
@@ -81,3 +89,27 @@ func syncUsers() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanExpiredData(db *gorm.DB) error {
|
||||
var fileErr error
|
||||
if err := db.Where("updated_at < ?", time.Now().Add(-time.Duration(config.Cfg.DB.Expire)*time.Second)).Unscoped().Delete(&ReceivedFile{}).Error; err != nil {
|
||||
fileErr = fmt.Errorf("failed to delete expired files: %w", err)
|
||||
}
|
||||
var cbErr error
|
||||
if err := db.Where("updated_at < ?", time.Now().Add(-time.Duration(config.Cfg.DB.Expire)*time.Second)).Unscoped().Delete(&CallbackData{}).Error; err != nil {
|
||||
cbErr = fmt.Errorf("failed to delete expired callback data: %w", err)
|
||||
}
|
||||
return errors.Join(fileErr, cbErr)
|
||||
}
|
||||
|
||||
func cleanJob(db *gorm.DB) {
|
||||
tick := time.NewTicker(time.Duration(config.Cfg.DB.Expire) * time.Second)
|
||||
defer tick.Stop()
|
||||
for range tick.C {
|
||||
if err := cleanExpiredData(db); err != nil {
|
||||
common.Log.Error("Failed to clean expired data: ", err)
|
||||
} else {
|
||||
common.Log.Debug("Cleaned expired data")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,3 +41,7 @@ func GetDirsByUserIDAndStorageName(userID uint, storageName string) ([]Dir, erro
|
||||
func DeleteDirForUser(userID uint, storageName, path string) error {
|
||||
return db.Unscoped().Where("user_id = ? AND storage_name = ? AND path = ?", userID, storageName, path).Delete(&Dir{}).Error
|
||||
}
|
||||
|
||||
func DeleteDirByID(id uint) error {
|
||||
return db.Unscoped().Delete(&Dir{}, id).Error
|
||||
}
|
||||
11
dao/model.go
11
dao/model.go
@@ -24,6 +24,8 @@ type User struct {
|
||||
Silent bool
|
||||
DefaultStorage string // Default storage name
|
||||
Dirs []Dir
|
||||
ApplyRule bool
|
||||
Rules []Rule
|
||||
}
|
||||
|
||||
type Dir struct {
|
||||
@@ -37,3 +39,12 @@ type CallbackData struct {
|
||||
gorm.Model
|
||||
Data string
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
Type string
|
||||
Data string
|
||||
StorageName string
|
||||
DirPath string
|
||||
}
|
||||
|
||||
22
dao/rule.go
Normal file
22
dao/rule.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package dao
|
||||
|
||||
func CreateRule(rule *Rule) error {
|
||||
return db.Create(rule).Error
|
||||
}
|
||||
|
||||
func DeleteRule(ruleID uint) error {
|
||||
return db.Unscoped().Delete(&Rule{}, ruleID).Error
|
||||
}
|
||||
|
||||
func UpdateUserApplyRule(chatID int64, applyRule bool) error {
|
||||
return db.Model(&User{}).Where("chat_id = ?", chatID).Update("apply_rule", applyRule).Error
|
||||
}
|
||||
|
||||
func GetRulesByUserChatID(chatID int64) ([]Rule, error) {
|
||||
var rules []Rule
|
||||
err := db.Where("user_id = (SELECT id FROM users WHERE chat_id = ?)", chatID).Find(&rules).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
@@ -9,7 +9,9 @@ func CreateUser(chatID int64) error {
|
||||
|
||||
func GetAllUsers() ([]User, error) {
|
||||
var users []User
|
||||
err := db.Preload("Dirs").Find(&users).Error
|
||||
err := db.Preload("Dirs").
|
||||
Preload("Rules").
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
@@ -17,6 +19,7 @@ func GetUserByChatID(chatID int64) (*User, error) {
|
||||
var user User
|
||||
err := db.
|
||||
Preload("Dirs").
|
||||
Preload("Rules").
|
||||
Where("chat_id = ?", chatID).First(&user).Error
|
||||
return &user, err
|
||||
}
|
||||
@@ -26,5 +29,5 @@ func UpdateUser(user *User) error {
|
||||
}
|
||||
|
||||
func DeleteUser(user *User) error {
|
||||
return db.Unscoped().Select("Dirs").Delete(user).Error
|
||||
return db.Unscoped().Select("Dirs", "Rules").Delete(user).Error
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@ Bot 接受两种消息: 文件和链接.
|
||||
|
||||
支持以下链接:
|
||||
|
||||
1. 公开频道 (具有用户名) 的消息链接, 例如: `https://t.me/acherkrau/1097`.
|
||||
|
||||
**即使频道禁止了转发和保存, Bot 依然可以下载其文件.**
|
||||
|
||||
1. 公开频道 (具有用户名) 的消息链接, 例如: `https://t.me/acherkrau/1097`. **即使频道禁止了转发和保存, Bot 依然可以下载其文件.**
|
||||
2. Telegra.ph 的文章链接, Bot 将下载其中的所有图片
|
||||
|
||||
## 静默模式 (silent)
|
||||
@@ -35,3 +32,7 @@ Bot 接受两种消息: 文件和链接.
|
||||
- 无法使用多线程从 telegram 下载文件, 速度较慢.
|
||||
- 网络不稳定时, 任务失败率高.
|
||||
- 无法在中间层对文件进行处理, 例如自动文件类型识别.
|
||||
|
||||
**不支持** Stream 模式的存储端:
|
||||
|
||||
- alist
|
||||
|
||||
13
go.mod
13
go.mod
@@ -6,6 +6,8 @@ require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/celestix/gotgproto v1.0.0-beta20.2
|
||||
github.com/celestix/telegraph-go/v2 v2.0.4
|
||||
github.com/eko/gocache/lib/v4 v4.2.0
|
||||
github.com/eko/gocache/store/go_cache/v4 v4.2.2
|
||||
github.com/gabriel-vasile/mimetype v1.4.8
|
||||
github.com/gookit/slog v0.5.7
|
||||
github.com/gotd/contrib v0.21.0
|
||||
@@ -20,6 +22,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/AnimeKaizoku/cacher v1.0.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
@@ -34,6 +37,7 @@ require (
|
||||
github.com/go-faster/yaml v0.4.6 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250128161936-077ca0a936bf // indirect
|
||||
@@ -47,10 +51,15 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/ogen-go/ogen v1.10.0 // indirect
|
||||
github.com/onsi/gomega v1.36.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
@@ -60,11 +69,13 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
modernc.org/libc v1.61.13 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
@@ -74,7 +85,6 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/coocood/freecache v1.2.4
|
||||
github.com/duke-git/lancet/v2 v2.3.4
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
@@ -86,6 +96,7 @@ require (
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
|
||||
51
go.sum
51
go.sum
@@ -1,5 +1,7 @@
|
||||
github.com/AnimeKaizoku/cacher v1.0.2 h1:7Bf5qRylWb7q2Evib0OXlhG37/t7BP2HK/7IyPvSmGQ=
|
||||
github.com/AnimeKaizoku/cacher v1.0.2/go.mod h1:jw0de/b0K6W7Y3T9rHCMGVKUf6oG7hENNcssxYcZTCc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/celestix/gotgproto v1.0.0-beta20.2 h1:+WcsKdsyj4xy+TAV+4Sw6zp1xiQrIr4dMnM31+k8NYM=
|
||||
@@ -8,13 +10,10 @@ github.com/celestix/telegraph-go/v2 v2.0.4 h1:w8HWymJFhMSMPjdGoyTh3/NqE3eXAT1njT
|
||||
github.com/celestix/telegraph-go/v2 v2.0.4/go.mod h1:vu2LtqM7MgOAJ2LDF8XK27DWdd1QYLBfZGhalEh086Y=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M=
|
||||
github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -25,6 +24,10 @@ github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvY
|
||||
github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw=
|
||||
github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M=
|
||||
github.com/eko/gocache/store/go_cache/v4 v4.2.2 h1:tAI9nl6TLoJyKG1ujF0CS0n/IgTEMl+NivxtR5R3/hw=
|
||||
github.com/eko/gocache/store/go_cache/v4 v4.2.2/go.mod h1:T9zkHokzr8K9EiC7RfMbDg6HSwaV6rv3UdcNu13SGcA=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -57,6 +60,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@@ -110,6 +115,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
@@ -122,6 +129,8 @@ github.com/minio/minio-go/v7 v7.0.81 h1:SzhMN0TQ6T/xSBu6Nvw3M5M8voM+Ht8RH3hE8S7z
|
||||
github.com/minio/minio-go/v7 v7.0.81/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ogen-go/ogen v1.10.0 h1:x3ukRtq/pdn/k8+pYBtqWceVASiSmgK9M5lrH89Q+04=
|
||||
@@ -130,12 +139,22 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
|
||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
|
||||
@@ -177,6 +196,7 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
@@ -189,22 +209,28 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -212,35 +238,50 @@ golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAG
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
@@ -106,6 +106,12 @@ 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(storagePath))
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
if length := ctx.Value(types.ContextKeyContentLength); length != nil {
|
||||
length, ok := length.(int64)
|
||||
if ok {
|
||||
req.ContentLength = length
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
@@ -134,6 +140,10 @@ func (a *Alist) Save(ctx context.Context, reader io.Reader, storagePath string)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Alist) NotSupportStream() string {
|
||||
return "Alist does not support chunked transfer encoding"
|
||||
}
|
||||
|
||||
func (a *Alist) JoinStoragePath(task types.Task) string {
|
||||
return path.Join(a.config.BasePath, task.StoragePath)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ type Storage interface {
|
||||
Save(ctx context.Context, reader io.Reader, storagePath string) error
|
||||
}
|
||||
|
||||
type StorageNotSupportStream interface {
|
||||
Storage
|
||||
NotSupportStream() string
|
||||
}
|
||||
|
||||
var Storages = make(map[string]Storage)
|
||||
|
||||
var UserStorages = make(map[int64][]Storage)
|
||||
|
||||
130
storage/webdav/client._test.go
Normal file
130
storage/webdav/client._test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
)
|
||||
|
||||
func setupWebDAVServer(t *testing.T) (*httptest.Server, string) {
|
||||
t.Helper()
|
||||
tempDir, err := os.MkdirTemp("", "webdav_test")
|
||||
if err != nil {
|
||||
t.Fatalf("mk temp dir failed: %v", err)
|
||||
}
|
||||
|
||||
handler := &webdav.Handler{
|
||||
Prefix: "/",
|
||||
FileSystem: webdav.Dir(tempDir),
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
}
|
||||
|
||||
server := httptest.NewServer(handler)
|
||||
return server, tempDir
|
||||
}
|
||||
|
||||
func TestMkDirAndExists(t *testing.T) {
|
||||
server, tempDir := setupWebDAVServer(t)
|
||||
defer os.RemoveAll(tempDir)
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "", "", nil)
|
||||
ctx := context.Background()
|
||||
|
||||
testpaths := []string{"testdir", "testdir/subdir", "testdir/子目录", "/testdir/测试路径/测试路径2"}
|
||||
for _, p := range testpaths {
|
||||
exists, err := client.Exists(ctx, p)
|
||||
if err != nil {
|
||||
t.Fatalf("Call Exists Err: %v", err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatalf("Dir should not exist")
|
||||
}
|
||||
|
||||
if err := client.MkDir(ctx, p); err != nil {
|
||||
t.Fatalf("Call MkDir Err: %v", err)
|
||||
}
|
||||
|
||||
exists, err = client.Exists(ctx, p)
|
||||
if err != nil {
|
||||
t.Fatalf("Call Exists Err: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatalf("Dir should exist")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
server, tempDir := setupWebDAVServer(t)
|
||||
defer os.RemoveAll(tempDir)
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "", "", nil)
|
||||
ctx := context.Background()
|
||||
|
||||
testCases := []struct {
|
||||
remotePath string
|
||||
content string
|
||||
}{
|
||||
{
|
||||
remotePath: "hello.txt",
|
||||
content: "Hello webdav",
|
||||
},
|
||||
{
|
||||
remotePath: "nested/dir/test.txt",
|
||||
content: "Nested file",
|
||||
},
|
||||
{
|
||||
remotePath: "empty.txt",
|
||||
content: "",
|
||||
},
|
||||
{
|
||||
remotePath: "unicode.txt",
|
||||
content: "测试",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.remotePath, func(t *testing.T) {
|
||||
dir := path.Dir(tc.remotePath)
|
||||
if dir != "." {
|
||||
if err := client.MkDir(ctx, dir); err != nil {
|
||||
t.Fatalf("创建目录 %s 失败: %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.WriteFile(ctx, tc.remotePath, strings.NewReader(tc.content)); err != nil {
|
||||
t.Fatalf("写入文件 %s 失败: %v", tc.remotePath, err)
|
||||
}
|
||||
|
||||
localPath := filepath.Join(tempDir, tc.remotePath)
|
||||
data, err := os.ReadFile(localPath)
|
||||
if err != nil {
|
||||
t.Fatalf("读取文件 %s 失败: %v", localPath, err)
|
||||
}
|
||||
if string(data) != tc.content {
|
||||
t.Fatalf("文件内容不匹配: got %s, want %s", string(data), tc.content)
|
||||
}
|
||||
|
||||
appended := tc.content + " Overwritten."
|
||||
if err := client.WriteFile(ctx, tc.remotePath, strings.NewReader(appended)); err != nil {
|
||||
t.Fatalf("覆盖写入文件 %s 失败: %v", tc.remotePath, err)
|
||||
}
|
||||
data, err = os.ReadFile(localPath)
|
||||
if err != nil {
|
||||
t.Fatalf("读取覆盖后的文件 %s 失败: %v", localPath, err)
|
||||
}
|
||||
if string(data) != appended {
|
||||
t.Fatalf("文件覆盖后的内容不匹配: got %s, want %s", string(data), appended)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -38,21 +40,63 @@ func (c *Client) doRequest(ctx context.Context, method, url string, body io.Read
|
||||
if c.Username != "" && c.Password != "" {
|
||||
req.SetBasicAuth(c.Username, c.Password)
|
||||
}
|
||||
if length := ctx.Value(types.ContextKeyContentLength); length != nil {
|
||||
if l, ok := length.(int64); ok {
|
||||
req.ContentLength = l
|
||||
}
|
||||
}
|
||||
return c.httpClient.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) MkDir(ctx context.Context, dirPath string) error {
|
||||
url := c.BaseURL + dirPath
|
||||
resp, err := c.doRequest(ctx, "MKCOL", url, nil)
|
||||
func (c *Client) Exists(ctx context.Context, remotePath string) (bool, error) {
|
||||
url := c.BaseURL + remotePath
|
||||
resp, err := c.doRequest(ctx, "PROPFIND", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return true, nil
|
||||
}
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("PROPFIND: %s", resp.Status)
|
||||
}
|
||||
|
||||
func (c *Client) MkDir(ctx context.Context, dirPath string) error {
|
||||
dirPath = strings.Trim(dirPath, "/")
|
||||
if dirPath == "" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("MKCOL: %s", resp.Status)
|
||||
parts := strings.Split(dirPath, "/")
|
||||
currentPath := ""
|
||||
for i, part := range parts {
|
||||
if i > 0 {
|
||||
currentPath += "/"
|
||||
}
|
||||
currentPath += part
|
||||
|
||||
exists, err := c.Exists(ctx, currentPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
url := c.BaseURL + currentPath
|
||||
resp, err := c.doRequest(ctx, "MKCOL", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("MKCOL %s: %s", currentPath, resp.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) WriteFile(ctx context.Context, remotePath string, content io.Reader) error {
|
||||
|
||||
82
types/task.go
Normal file
82
types/task.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
Error error
|
||||
Status TaskStatus
|
||||
StorageName string
|
||||
StoragePath string
|
||||
StartTime time.Time
|
||||
|
||||
File *File
|
||||
FileMessageID int
|
||||
FileChatID int64
|
||||
|
||||
IsTelegraph bool
|
||||
TelegraphURL string
|
||||
|
||||
// to track the reply message
|
||||
ReplyMessageID int
|
||||
ReplyChatID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
func (t Task) Key() string {
|
||||
if t.IsTelegraph {
|
||||
return hashStr(t.TelegraphURL)
|
||||
}
|
||||
return fmt.Sprintf("%d:%d", t.FileChatID, t.FileMessageID)
|
||||
}
|
||||
|
||||
func (t Task) String() string {
|
||||
if t.IsTelegraph {
|
||||
return fmt.Sprintf("[telegraph]:%s", t.TelegraphURL)
|
||||
}
|
||||
return fmt.Sprintf("[%d:%d]:%s", t.FileChatID, t.FileMessageID, t.File.FileName)
|
||||
}
|
||||
|
||||
func (t Task) FileName() string {
|
||||
if t.IsTelegraph {
|
||||
tgphPath := strings.Split(t.TelegraphURL, "/")[len(strings.Split(t.TelegraphURL, "/"))-1]
|
||||
tgphPathUnescaped, err := url.PathUnescape(tgphPath)
|
||||
if err != nil {
|
||||
return tgphPath
|
||||
}
|
||||
return tgphPathUnescaped
|
||||
}
|
||||
return t.File.FileName
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Location tg.InputFileLocationClass
|
||||
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)
|
||||
}
|
||||
@@ -1,20 +1,8 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
)
|
||||
|
||||
type TaskStatus string
|
||||
|
||||
var (
|
||||
const (
|
||||
Pending TaskStatus = "pending"
|
||||
Succeeded TaskStatus = "succeeded"
|
||||
Failed TaskStatus = "failed"
|
||||
@@ -23,7 +11,7 @@ var (
|
||||
|
||||
type StorageType string
|
||||
|
||||
var (
|
||||
const (
|
||||
StorageTypeLocal StorageType = "local"
|
||||
StorageTypeWebdav StorageType = "webdav"
|
||||
StorageTypeAlist StorageType = "alist"
|
||||
@@ -38,71 +26,17 @@ var StorageTypeDisplay = map[StorageType]string{
|
||||
StorageTypeMinio: "Minio",
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
Error error
|
||||
Status TaskStatus
|
||||
StorageName string
|
||||
StoragePath string
|
||||
StartTime time.Time
|
||||
type ContextKey string
|
||||
|
||||
File *File
|
||||
FileMessageID int
|
||||
FileChatID int64
|
||||
const (
|
||||
ContextKeyContentLength ContextKey = "content-length"
|
||||
)
|
||||
|
||||
IsTelegraph bool
|
||||
TelegraphURL string
|
||||
type RuleType string
|
||||
|
||||
// to track the reply message
|
||||
ReplyMessageID int
|
||||
ReplyChatID int64
|
||||
UserID int64
|
||||
}
|
||||
const (
|
||||
RuleTypeFileNameRegex RuleType = "FILENAME-REGEX"
|
||||
RuleTypeMessageRegex RuleType = "MESSAGE-REGEX"
|
||||
)
|
||||
|
||||
func (t Task) Key() string {
|
||||
if t.IsTelegraph {
|
||||
return hashStr(t.TelegraphURL)
|
||||
}
|
||||
return fmt.Sprintf("%d:%d", t.FileChatID, t.FileMessageID)
|
||||
}
|
||||
|
||||
func (t Task) String() string {
|
||||
if t.IsTelegraph {
|
||||
return fmt.Sprintf("[telegraph]:%s", t.TelegraphURL)
|
||||
}
|
||||
return fmt.Sprintf("[%d:%d]:%s", t.FileChatID, t.FileMessageID, t.File.FileName)
|
||||
}
|
||||
|
||||
func (t Task) FileName() string {
|
||||
if t.IsTelegraph {
|
||||
tgphPath := strings.Split(t.TelegraphURL, "/")[len(strings.Split(t.TelegraphURL, "/"))-1]
|
||||
tgphPathUnescaped, err := url.PathUnescape(tgphPath)
|
||||
if err != nil {
|
||||
return tgphPath
|
||||
}
|
||||
return tgphPathUnescaped
|
||||
}
|
||||
return t.File.FileName
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Location tg.InputFileLocationClass
|
||||
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)
|
||||
}
|
||||
var RuleTypes = []RuleType{RuleTypeFileNameRegex, RuleTypeMessageRegex}
|
||||
Reference in New Issue
Block a user