2 |
3 | #

Save Any Bot
4 |
5 | [简体中文](README.md) | **English**
6 |
7 | Save Telegram files to various storage endpoints.
8 |
9 | > _Just like PikPak Bot_
10 |
11 |
12 |
13 | ## Deployment
14 |
15 | ### Deploy from Binary
16 |
17 | Download the binary file for your platform from the [Release](https://github.com/krau/SaveAny-Bot/releases) page.
18 |
19 | Create a `config.toml` file in the extracted directory, refer to [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) for configuration.
20 |
21 | Run:
22 |
23 | ```bash
24 | chmod +x saveany-bot
25 | ./saveany-bot
26 | ```
27 |
28 | #### Add as systemd Service
29 |
30 | Create file `/etc/systemd/system/saveany-bot.service` and write the following content:
31 |
32 | ```
33 | [Unit]
34 | Description=SaveAnyBot
35 | After=systemd-user-sessions.service
36 |
37 | [Service]
38 | Type=simple
39 | WorkingDirectory=/yourpath/
40 | ExecStart=/yourpath/saveany-bot
41 | Restart=on-failure
42 |
43 | [Install]
44 | WantedBy=multi-user.target
45 | ```
46 |
47 | Enable auto-start and start the service:
48 |
49 | ```bash
50 | systemctl enable --now saveany-bot
51 | ```
52 |
53 | ### Deploy with Docker
54 |
55 | #### Docker Compose
56 |
57 | Download [docker-compose.yml](https://github.com/krau/SaveAny-Bot/blob/main/docker-compose.yml) file and create a `config.toml` file in the same directory, refer to [config.example.toml](https://github.com/krau/SaveAny-Bot/blob/main/config.example.toml) for configuration.
58 |
59 | Run:
60 |
61 | ```bash
62 | docker compose up -d
63 | ```
64 |
65 | #### Docker
66 |
67 | ```shell
68 | docker run -d --name saveany-bot \
69 | -v /path/to/config.toml:/app/config.toml \
70 | -v /path/to/downloads:/app/downloads \
71 | ghcr.io/krau/saveany-bot:latest
72 | ```
73 |
74 | ## Update
75 |
76 | Use `upgrade` or `up` command to upgrade to the latest version:
77 |
78 | ```bash
79 | ./saveany-bot upgrade
80 | ```
81 |
82 | If deployed with Docker, use the following commands to update:
83 |
84 | ```bash
85 | docker pull ghcr.io/krau/saveany-bot:latest
86 | docker restart saveany-bot
87 | ```
88 |
89 | ## Usage
90 |
91 | Send (forward) files to the Bot and follow the prompts.
92 |
93 | ---
94 |
95 | ## Sponsors
96 |
97 | This project is supported by [YxVM](https://yxvm.com/) and [NodeSupport](https://github.com/NodeSeekDev/NodeSupport).
98 |
99 | You can consider sponsoring me if this project helps you:
100 |
101 | - [Afdian](https://afdian.com/a/acherkrau)
102 |
103 | ## Thanks
104 |
105 | - [gotd](https://github.com/gotd/td)
106 | - [TG-FileStreamBot](https://github.com/EverythingSuckz/TG-FileStreamBot)
107 | - [gotgproto](https://github.com/celestix/gotgproto)
108 | - All the dependencies
--------------------------------------------------------------------------------
/bot/bot.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "context"
5 | "net/url"
6 | "time"
7 |
8 | "github.com/celestix/gotgproto"
9 | "github.com/celestix/gotgproto/sessionMaker"
10 | "github.com/glebarez/sqlite"
11 | "github.com/gotd/td/telegram/dcs"
12 | "github.com/gotd/td/tg"
13 | "github.com/krau/SaveAny-Bot/common"
14 | "github.com/krau/SaveAny-Bot/config"
15 | "golang.org/x/net/proxy"
16 | )
17 |
18 | var Client *gotgproto.Client
19 |
20 | func newProxyDialer(proxyUrl string) (proxy.Dialer, error) {
21 | url, err := url.Parse(proxyUrl)
22 | if err != nil {
23 | return nil, err
24 | }
25 | return proxy.FromURL(url, proxy.Direct)
26 | }
27 |
28 | func Init() {
29 | common.Log.Info("初始化 Telegram 客户端...")
30 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Cfg.Telegram.Timeout)*time.Second)
31 | defer cancel()
32 | go InitTelegraphClient()
33 | resultChan := make(chan struct {
34 | client *gotgproto.Client
35 | err error
36 | })
37 | go func() {
38 | var resolver dcs.Resolver
39 | if config.Cfg.Telegram.Proxy.Enable && config.Cfg.Telegram.Proxy.URL != "" {
40 | dialer, err := newProxyDialer(config.Cfg.Telegram.Proxy.URL)
41 | if err != nil {
42 | resultChan <- struct {
43 | client *gotgproto.Client
44 | err error
45 | }{nil, err}
46 | return
47 | }
48 | resolver = dcs.Plain(dcs.PlainOptions{
49 | Dial: dialer.(proxy.ContextDialer).DialContext,
50 | })
51 | } else {
52 | resolver = dcs.DefaultResolver()
53 | }
54 | client, err := gotgproto.NewClient(config.Cfg.Telegram.AppID,
55 | config.Cfg.Telegram.AppHash,
56 | gotgproto.ClientTypeBot(config.Cfg.Telegram.Token),
57 | &gotgproto.ClientOpts{
58 | Session: sessionMaker.SqlSession(sqlite.Open(config.Cfg.DB.Session)),
59 | DisableCopyright: true,
60 | Middlewares: FloodWaitMiddleware(),
61 | Resolver: resolver,
62 | MaxRetries: config.Cfg.Telegram.RpcRetry,
63 | },
64 | )
65 | if err != nil {
66 | resultChan <- struct {
67 | client *gotgproto.Client
68 | err error
69 | }{nil, err}
70 | return
71 | }
72 | _, err = client.API().BotsSetBotCommands(ctx, &tg.BotsSetBotCommandsRequest{
73 | Scope: &tg.BotCommandScopeDefault{},
74 | Commands: []tg.BotCommand{
75 | {Command: "start", Description: "开始使用"},
76 | {Command: "help", Description: "显示帮助"},
77 | {Command: "silent", Description: "开启/关闭静默模式"},
78 | {Command: "storage", Description: "设置默认存储端"},
79 | {Command: "save", Description: "保存所回复的文件"},
80 | {Command: "dir", Description: "管理存储文件夹"},
81 | {Command: "rule", Description: "管理规则"},
82 | },
83 | })
84 | resultChan <- struct {
85 | client *gotgproto.Client
86 | err error
87 | }{client, err}
88 | }()
89 |
90 | select {
91 | case <-ctx.Done():
92 | common.Log.Panic("初始化客户端失败: 超时")
93 | case result := <-resultChan:
94 | if result.err != nil {
95 | common.Log.Panicf("初始化客户端失败: %s", result.err)
96 | }
97 | Client = result.client
98 | RegisterHandlers(Client.Dispatcher)
99 | common.Log.Info("客户端初始化完成")
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/bot/handle_add_task.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "path"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/celestix/gotgproto/dispatcher"
11 | "github.com/celestix/gotgproto/ext"
12 | "github.com/duke-git/lancet/v2/slice"
13 | "github.com/gotd/td/telegram/message/entity"
14 | "github.com/gotd/td/telegram/message/styling"
15 | "github.com/gotd/td/tg"
16 | "github.com/krau/SaveAny-Bot/common"
17 | "github.com/krau/SaveAny-Bot/config"
18 | "github.com/krau/SaveAny-Bot/dao"
19 | "github.com/krau/SaveAny-Bot/queue"
20 | "github.com/krau/SaveAny-Bot/types"
21 | "gorm.io/gorm"
22 | )
23 |
24 | func AddToQueue(ctx *ext.Context, update *ext.Update) error {
25 | if !slice.Contain(config.Cfg.GetUsersID(), update.CallbackQuery.UserID) {
26 | ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
27 | QueryID: update.CallbackQuery.QueryID,
28 | Alert: true,
29 | Message: "你没有权限",
30 | CacheTime: 5,
31 | })
32 | return dispatcher.EndGroups
33 | }
34 | args := strings.Split(string(update.CallbackQuery.Data), " ")
35 | addToDir := args[0] == "add_to_dir" // 已经选择了路径
36 | cbDataId, _ := strconv.Atoi(args[1])
37 | cbData, err := dao.GetCallbackData(uint(cbDataId))
38 | if err != nil {
39 | common.Log.Errorf("获取回调数据失败: %s", err)
40 | ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
41 | QueryID: update.CallbackQuery.QueryID,
42 | Alert: true,
43 | Message: "获取回调数据失败",
44 | CacheTime: 5,
45 | })
46 | return dispatcher.EndGroups
47 | }
48 |
49 | data := strings.Split(cbData, " ")
50 | fileChatID, _ := strconv.Atoi(data[0])
51 | fileMessageID, _ := strconv.Atoi(data[1])
52 | storageName := data[2]
53 | dirIdInt, _ := strconv.Atoi(data[3])
54 | dirId := uint(dirIdInt)
55 |
56 | user, err := dao.GetUserByChatID(update.CallbackQuery.UserID)
57 | if err != nil {
58 | common.Log.Errorf("获取用户失败: %s", err)
59 | ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
60 | QueryID: update.CallbackQuery.QueryID,
61 | Alert: true,
62 | Message: "获取用户失败",
63 | CacheTime: 5,
64 | })
65 | return dispatcher.EndGroups
66 | }
67 |
68 | if !addToDir {
69 | dirs, err := dao.GetDirsByUserIDAndStorageName(user.ID, storageName)
70 | if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
71 | common.Log.Errorf("获取路径失败: %s", err)
72 | ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
73 | QueryID: update.CallbackQuery.QueryID,
74 | Alert: true,
75 | Message: "获取路径失败",
76 | CacheTime: 5,
77 | })
78 | return dispatcher.EndGroups
79 | }
80 | if len(dirs) != 0 {
81 | markup, err := getSelectDirMarkup(fileChatID, fileMessageID, storageName, dirs)
82 | if err != nil {
83 | common.Log.Errorf("获取路径失败: %s", err)
84 | ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
85 | QueryID: update.CallbackQuery.QueryID,
86 | Alert: true,
87 | Message: "获取路径失败",
88 | CacheTime: 5,
89 | })
90 | return dispatcher.EndGroups
91 | }
92 | _, err = ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
93 | ID: update.CallbackQuery.GetMsgID(),
94 | Message: "请选择要保存到的路径",
95 | ReplyMarkup: markup,
96 | })
97 | if err != nil {
98 | common.Log.Errorf("编辑消息失败: %s", err)
99 | }
100 | return dispatcher.EndGroups
101 | }
102 | }
103 |
104 | common.Log.Tracef("Got add to queue: chatID: %d, messageID: %d, storage: %s", fileChatID, fileMessageID, storageName)
105 | record, err := dao.GetReceivedFileByChatAndMessageID(int64(fileChatID), fileMessageID)
106 | if err != nil {
107 | common.Log.Errorf("获取记录失败: %s", err)
108 | ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
109 | QueryID: update.CallbackQuery.QueryID,
110 | Alert: true,
111 | Message: "查询记录失败",
112 | CacheTime: 5,
113 | })
114 | return dispatcher.EndGroups
115 | }
116 | if update.CallbackQuery.MsgID != record.ReplyMessageID {
117 | record.ReplyMessageID = update.CallbackQuery.MsgID
118 | if _, err := dao.SaveReceivedFile(record); err != nil {
119 | common.Log.Errorf("更新记录失败: %s", err)
120 | }
121 | }
122 |
123 | var dir *dao.Dir
124 | if addToDir && dirId != 0 {
125 | dir, err = dao.GetDirByID(dirId)
126 | if err != nil {
127 | common.Log.Errorf("获取路径失败: %s", err)
128 | ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
129 | QueryID: update.CallbackQuery.QueryID,
130 | Alert: true,
131 | Message: "获取路径失败",
132 | CacheTime: 5,
133 | })
134 | return dispatcher.EndGroups
135 | }
136 | }
137 |
138 | var task types.Task
139 | if record.IsTelegraph {
140 | task = types.Task{
141 | Ctx: ctx,
142 | Status: types.Pending,
143 | IsTelegraph: true,
144 | TelegraphURL: record.TelegraphURL,
145 | StorageName: storageName,
146 | FileChatID: record.ChatID,
147 | FileMessageID: record.MessageID,
148 | ReplyMessageID: record.ReplyMessageID,
149 | ReplyChatID: record.ReplyChatID,
150 | UserID: update.GetUserChat().GetID(),
151 | }
152 | if dir != nil {
153 | task.StoragePath = path.Join(dir.Path, record.FileName)
154 | }
155 | } else {
156 | file, err := FileFromMessage(ctx, record.ChatID, record.MessageID, record.FileName)
157 | if err != nil {
158 | common.Log.Errorf("获取消息中的文件失败: %s", err)
159 | ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
160 | QueryID: update.CallbackQuery.QueryID,
161 | Alert: true,
162 | Message: fmt.Sprintf("获取消息中的文件失败: %s", err),
163 | CacheTime: 5,
164 | })
165 | return dispatcher.EndGroups
166 | }
167 |
168 | task = types.Task{
169 | Ctx: ctx,
170 | Status: types.Pending,
171 | FileDBID: record.ID,
172 | File: file,
173 | StorageName: storageName,
174 | FileChatID: record.ChatID,
175 | ReplyMessageID: record.ReplyMessageID,
176 | FileMessageID: record.MessageID,
177 | ReplyChatID: record.ReplyChatID,
178 | UserID: update.GetUserChat().GetID(),
179 | }
180 | if dir != nil {
181 | task.StoragePath = path.Join(dir.Path, file.FileName)
182 | }
183 | }
184 |
185 | queue.AddTask(&task)
186 |
187 | entityBuilder := entity.Builder{}
188 | var entities []tg.MessageEntityClass
189 | text := fmt.Sprintf("已添加到任务队列\n文件名: %s\n当前排队任务数: %d", record.FileName, queue.Len())
190 | if err := styling.Perform(&entityBuilder,
191 | styling.Plain("已添加到任务队列\n文件名: "),
192 | styling.Code(record.FileName),
193 | styling.Plain("\n当前排队任务数: "),
194 | styling.Bold(strconv.Itoa(queue.Len())),
195 | ); err != nil {
196 | common.Log.Errorf("Failed to build entity: %s", err)
197 | } else {
198 | text, entities = entityBuilder.Complete()
199 | }
200 |
201 | ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
202 | Message: text,
203 | Entities: entities,
204 | ID: record.ReplyMessageID,
205 | })
206 | return dispatcher.EndGroups
207 | }
208 |
--------------------------------------------------------------------------------
/bot/handle_cancel_task.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/celestix/gotgproto/dispatcher"
7 | "github.com/celestix/gotgproto/ext"
8 | "github.com/gotd/td/tg"
9 | "github.com/krau/SaveAny-Bot/queue"
10 | )
11 |
12 | func cancelTask(ctx *ext.Context, update *ext.Update) error {
13 | key := strings.Split(string(update.CallbackQuery.Data), " ")[1]
14 | ok := queue.CancelTask(key)
15 | if ok {
16 | ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
17 | QueryID: update.CallbackQuery.QueryID,
18 | Message: "任务已取消",
19 | })
20 | return dispatcher.EndGroups
21 | }
22 | ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
23 | QueryID: update.CallbackQuery.QueryID,
24 | Message: "任务取消失败",
25 | })
26 | return dispatcher.EndGroups
27 | }
28 |
--------------------------------------------------------------------------------
/bot/handle_dir.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/celestix/gotgproto/dispatcher"
9 | "github.com/celestix/gotgproto/ext"
10 | "github.com/gotd/td/telegram/message/styling"
11 | "github.com/krau/SaveAny-Bot/common"
12 | "github.com/krau/SaveAny-Bot/dao"
13 | "github.com/krau/SaveAny-Bot/storage"
14 | )
15 |
16 | func sendDirHelp(ctx *ext.Context, update *ext.Update, userChatID int64) error {
17 | dirs, err := dao.GetUserDirsByChatID(userChatID)
18 | if err != nil {
19 | common.Log.Errorf("获取用户路径失败: %s", err)
20 | ctx.Reply(update, ext.ReplyTextString("获取用户路径失败"), nil)
21 | return dispatcher.EndGroups
22 | }
23 | ctx.Reply(update, ext.ReplyTextStyledTextArray(
24 | []styling.StyledTextOption{
25 | styling.Bold("使用方法: /dir <操作> <参数...>"),
26 | styling.Plain("\n\n可用操作:\n"),
27 | styling.Code("add"),
28 | styling.Plain(" <存储名> <路径> - 添加路径\n"),
29 | styling.Code("del"),
30 | styling.Plain(" <路径ID> - 删除路径\n"),
31 | styling.Plain("\n添加路径示例:\n"),
32 | styling.Code("/dir add local1 path/to/dir"),
33 | styling.Plain("\n\n删除路径示例:\n"),
34 | styling.Code("/dir del 3"),
35 | styling.Plain("\n\n当前已添加的路径:\n"),
36 | styling.Blockquote(func() string {
37 | var sb strings.Builder
38 | for _, dir := range dirs {
39 | sb.WriteString(fmt.Sprintf("%d: ", dir.ID))
40 | sb.WriteString(dir.StorageName)
41 | sb.WriteString(" - ")
42 | sb.WriteString(dir.Path)
43 | sb.WriteString("\n")
44 | }
45 | return sb.String()
46 | }(), true),
47 | },
48 | ), nil)
49 | return dispatcher.EndGroups
50 | }
51 |
52 | func dirCmd(ctx *ext.Context, update *ext.Update) error {
53 | args := strings.Split(update.EffectiveMessage.Text, " ")
54 | if len(args) < 2 {
55 | return sendDirHelp(ctx, update, update.GetUserChat().GetID())
56 | }
57 | user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
58 | if err != nil {
59 | common.Log.Errorf("获取用户失败: %s", err)
60 | ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
61 | return dispatcher.EndGroups
62 | }
63 | switch args[1] {
64 | case "add":
65 | // /dir add local1 path/to/dir
66 | if len(args) < 4 {
67 | return sendDirHelp(ctx, update, update.GetUserChat().GetID())
68 | }
69 | return addDir(ctx, update, user, args[2], args[3])
70 | case "del":
71 | // /dir del 3
72 | if len(args) < 3 {
73 | return sendDirHelp(ctx, update, update.GetUserChat().GetID())
74 | }
75 | dirID, err := strconv.Atoi(args[2])
76 | if err != nil {
77 | ctx.Reply(update, ext.ReplyTextString("路径ID无效"), nil)
78 | return dispatcher.EndGroups
79 | }
80 | return delDir(ctx, update, dirID)
81 | default:
82 | ctx.Reply(update, ext.ReplyTextString("未知操作"), nil)
83 | return dispatcher.EndGroups
84 | }
85 | }
86 |
87 | func addDir(ctx *ext.Context, update *ext.Update, user *dao.User, storageName, path string) error {
88 | if _, err := storage.GetStorageByUserIDAndName(user.ChatID, storageName); err != nil {
89 | ctx.Reply(update, ext.ReplyTextString(err.Error()), nil)
90 | return dispatcher.EndGroups
91 | }
92 |
93 | if err := dao.CreateDirForUser(user.ID, storageName, path); err != nil {
94 | common.Log.Errorf("创建路径失败: %s", err)
95 | ctx.Reply(update, ext.ReplyTextString("创建路径失败"), nil)
96 | return dispatcher.EndGroups
97 | }
98 | ctx.Reply(update, ext.ReplyTextString("路径添加成功"), nil)
99 | return dispatcher.EndGroups
100 | }
101 |
102 | func delDir(ctx *ext.Context, update *ext.Update, dirID int) error {
103 | if err := dao.DeleteDirByID(uint(dirID)); err != nil {
104 | common.Log.Errorf("删除路径失败: %s", err)
105 | ctx.Reply(update, ext.ReplyTextString("删除路径失败"), nil)
106 | return dispatcher.EndGroups
107 | }
108 | ctx.Reply(update, ext.ReplyTextString("路径删除成功"), nil)
109 | return dispatcher.EndGroups
110 | }
111 |
--------------------------------------------------------------------------------
/bot/handle_file.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/celestix/gotgproto/dispatcher"
7 | "github.com/celestix/gotgproto/ext"
8 | "github.com/gotd/td/tg"
9 | "github.com/krau/SaveAny-Bot/common"
10 | "github.com/krau/SaveAny-Bot/dao"
11 | "github.com/krau/SaveAny-Bot/types"
12 | )
13 |
14 | func handleFileMessage(ctx *ext.Context, update *ext.Update) error {
15 | common.Log.Trace("Got media: ", update.EffectiveMessage.Media.TypeName())
16 | supported, err := supportedMediaFilter(update.EffectiveMessage.Message)
17 | if err != nil {
18 | return err
19 | }
20 | if !supported {
21 | return dispatcher.EndGroups
22 | }
23 |
24 | user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
25 | if err != nil {
26 | common.Log.Errorf("获取用户失败: %s", err)
27 | ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
28 | return dispatcher.EndGroups
29 | }
30 | // storages := storage.GetUserStorages(user.ChatID)
31 | // if len(storages) == 0 {
32 | // ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
33 | // return dispatcher.EndGroups
34 | // }
35 |
36 | msg, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件信息..."), nil)
37 | if err != nil {
38 | common.Log.Errorf("回复失败: %s", err)
39 | return dispatcher.EndGroups
40 | }
41 | media := update.EffectiveMessage.Media
42 | file, err := FileFromMedia(media, "")
43 | if err != nil {
44 | common.Log.Errorf("获取文件失败: %s", err)
45 | ctx.Reply(update, ext.ReplyTextString(fmt.Sprintf("获取文件失败: %s", err)), nil)
46 | return dispatcher.EndGroups
47 | }
48 | if file.FileName == "" {
49 | file.FileName = GenFileNameFromMessage(*update.EffectiveMessage.Message, file)
50 | }
51 |
52 | record, err := dao.SaveReceivedFile(&dao.ReceivedFile{
53 | Processing: false,
54 | FileName: file.FileName,
55 | ChatID: update.EffectiveChat().GetID(),
56 | MessageID: update.EffectiveMessage.ID,
57 | ReplyMessageID: msg.ID,
58 | ReplyChatID: update.GetUserChat().GetID(),
59 | })
60 | if err != nil {
61 | common.Log.Errorf("添加接收的文件失败: %s", err)
62 | if _, err := ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
63 | Message: fmt.Sprintf("添加接收的文件失败: %s", err),
64 | ID: msg.ID,
65 | }); err != nil {
66 | common.Log.Errorf("编辑消息失败: %s", err)
67 | }
68 | return dispatcher.EndGroups
69 | }
70 |
71 | if !user.Silent || user.DefaultStorage == "" {
72 | return ProvideSelectMessage(ctx, update, file.FileName, update.EffectiveChat().GetID(), update.EffectiveMessage.ID, msg.ID)
73 | }
74 | return HandleSilentAddTask(ctx, update, user, &types.Task{
75 | Ctx: ctx,
76 | Status: types.Pending,
77 | FileDBID: record.ID,
78 | File: file,
79 | StorageName: user.DefaultStorage,
80 | FileChatID: update.EffectiveChat().GetID(),
81 | ReplyMessageID: msg.ID,
82 | ReplyChatID: update.GetUserChat().GetID(),
83 | FileMessageID: update.EffectiveMessage.ID,
84 | UserID: user.ChatID,
85 | })
86 | }
87 |
--------------------------------------------------------------------------------
/bot/handle_link.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/celestix/gotgproto/dispatcher"
10 | "github.com/celestix/gotgproto/ext"
11 | "github.com/gotd/td/tg"
12 | "github.com/krau/SaveAny-Bot/common"
13 | "github.com/krau/SaveAny-Bot/dao"
14 | "github.com/krau/SaveAny-Bot/types"
15 | )
16 |
17 | var (
18 | linkRegexString = `t.me/.*/\d+`
19 | linkRegex = regexp.MustCompile(linkRegexString)
20 | )
21 |
22 | func parseLink(ctx *ext.Context, link string) (chatID int64, messageID int, err error) {
23 | strSlice := strings.Split(link, "/")
24 | if len(strSlice) < 3 {
25 | return 0, 0, fmt.Errorf("链接格式错误: %s", link)
26 | }
27 | messageID, err = strconv.Atoi(strSlice[len(strSlice)-1])
28 | if err != nil {
29 | return 0, 0, fmt.Errorf("无法解析消息 ID: %s", err)
30 | }
31 | if len(strSlice) == 3 {
32 | chatUsername := strSlice[1]
33 | linkChat, err := ctx.ResolveUsername(chatUsername)
34 | if err != nil {
35 | return 0, 0, fmt.Errorf("解析用户名失败: %s", err)
36 | }
37 | if linkChat == nil {
38 | return 0, 0, fmt.Errorf("找不到该聊天: %s", chatUsername)
39 | }
40 | chatID = linkChat.GetID()
41 | } else if len(strSlice) == 4 {
42 | chatIDInt, err := strconv.Atoi(strSlice[2])
43 | if err != nil {
44 | return 0, 0, fmt.Errorf("无法解析 Chat ID: %s", err)
45 | }
46 | chatID = int64(chatIDInt)
47 | } else {
48 | return 0, 0, fmt.Errorf("无效的链接: %s", link)
49 | }
50 | return chatID, messageID, nil
51 | }
52 |
53 | func handleLinkMessage(ctx *ext.Context, update *ext.Update) error {
54 | common.Log.Trace("Got link message")
55 | link := linkRegex.FindString(update.EffectiveMessage.Text)
56 | if link == "" {
57 | return dispatcher.ContinueGroups
58 | }
59 | linkChatID, messageID, err := parseLink(ctx, link)
60 | if err != nil {
61 | common.Log.Errorf("解析链接失败: %s", err)
62 | ctx.Reply(update, ext.ReplyTextString("解析链接失败: "+err.Error()), nil)
63 | return dispatcher.EndGroups
64 | }
65 |
66 | user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
67 | if err != nil {
68 | common.Log.Errorf("获取用户失败: %s", err)
69 | ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
70 | return dispatcher.EndGroups
71 | }
72 |
73 | // storages := storage.GetUserStorages(user.ChatID)
74 | // if len(storages) == 0 {
75 | // ctx.Reply(update, ext.ReplyTextString("无可用的存储"), nil)
76 | // return dispatcher.EndGroups
77 | // }
78 |
79 | replied, err := ctx.Reply(update, ext.ReplyTextString("正在获取文件..."), nil)
80 | if err != nil {
81 | common.Log.Errorf("回复失败: %s", err)
82 | return dispatcher.EndGroups
83 | }
84 |
85 | file, err := FileFromMessage(ctx, linkChatID, messageID, "")
86 | if err != nil {
87 | common.Log.Errorf("获取文件失败: %s", err)
88 | ctx.Reply(update, ext.ReplyTextString("获取文件失败: "+err.Error()), nil)
89 | return dispatcher.EndGroups
90 | }
91 | if file.FileName == "" {
92 | file.FileName = GenFileNameFromMessage(*update.EffectiveMessage.Message, file)
93 | }
94 |
95 | receivedFile := &dao.ReceivedFile{
96 | Processing: false,
97 | FileName: file.FileName,
98 | ChatID: linkChatID,
99 | MessageID: messageID,
100 | ReplyMessageID: replied.ID,
101 | ReplyChatID: update.GetUserChat().GetID(),
102 | }
103 | record, err := dao.SaveReceivedFile(receivedFile)
104 | if err != nil {
105 | common.Log.Errorf("保存接收的文件失败: %s", err)
106 | ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
107 | Message: "无法保存文件: " + err.Error(),
108 | ID: replied.ID,
109 | })
110 | return dispatcher.EndGroups
111 | }
112 | if !user.Silent || user.DefaultStorage == "" {
113 | return ProvideSelectMessage(ctx, update, file.FileName, linkChatID, messageID, replied.ID)
114 | }
115 | return HandleSilentAddTask(ctx, update, user, &types.Task{
116 | Ctx: ctx,
117 | Status: types.Pending,
118 | FileDBID: record.ID,
119 | File: file,
120 | StorageName: user.DefaultStorage,
121 | UserID: user.ChatID,
122 | FileChatID: linkChatID,
123 | FileMessageID: messageID,
124 | ReplyMessageID: replied.ID,
125 | ReplyChatID: update.GetUserChat().GetID(),
126 | })
127 | }
128 |
--------------------------------------------------------------------------------
/bot/handle_rule.go:
--------------------------------------------------------------------------------
1 | package bot
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/celestix/gotgproto/dispatcher"
9 | "github.com/celestix/gotgproto/ext"
10 | "github.com/duke-git/lancet/v2/slice"
11 | "github.com/gotd/td/telegram/message/styling"
12 | "github.com/krau/SaveAny-Bot/common"
13 | "github.com/krau/SaveAny-Bot/dao"
14 | "github.com/krau/SaveAny-Bot/types"
15 | )
16 |
17 | func sendRuleHelp(ctx *ext.Context, update *ext.Update, userChatID int64) error {
18 | user, err := dao.GetUserByChatID(userChatID)
19 | if err != nil {
20 | common.Log.Errorf("获取用户规则失败: %s", err)
21 | ctx.Reply(update, ext.ReplyTextString("获取用户规则失败"), nil)
22 | return dispatcher.EndGroups
23 | }
24 | ctx.Reply(update, ext.ReplyTextStyledTextArray(
25 | []styling.StyledTextOption{
26 | styling.Bold("使用方法: /rule <操作> <参数...>"),
27 | styling.Bold(fmt.Sprintf("\n当前已%s规则模式", map[bool]string{true: "启用", false: "禁用"}[user.ApplyRule])),
28 | styling.Plain("\n\n可用操作:\n"),
29 | styling.Code("switch"),
30 | styling.Plain(" - 开关规则模式\n"),
31 | styling.Code("add"),
32 | styling.Plain(" <类型> <数据> <存储名> <路径> - 添加规则\n"),
33 | styling.Code("del"),
34 | styling.Plain(" <规则ID> - 删除规则\n"),
35 | styling.Plain("\n当前已添加的规则:\n"),
36 | styling.Blockquote(func() string {
37 | var sb strings.Builder
38 | for _, rule := range user.Rules {
39 | ruleText := fmt.Sprintf("%s %s %s %s", rule.Type, rule.Data, rule.StorageName, rule.DirPath)
40 | sb.WriteString(fmt.Sprintf("%d: %s\n", rule.ID, ruleText))
41 | }
42 | return sb.String()
43 | }(), true),
44 | },
45 | ), nil)
46 | return dispatcher.EndGroups
47 | }
48 |
49 | func ruleCmd(ctx *ext.Context, update *ext.Update) error {
50 | args := strings.Split(update.EffectiveMessage.Text, " ")
51 | if len(args) < 2 {
52 | return sendRuleHelp(ctx, update, update.GetUserChat().GetID())
53 | }
54 | user, err := dao.GetUserByChatID(update.GetUserChat().GetID())
55 | if err != nil {
56 | common.Log.Errorf("获取用户失败: %s", err)
57 | ctx.Reply(update, ext.ReplyTextString("获取用户失败"), nil)
58 | return dispatcher.EndGroups
59 | }
60 | switch args[1] {
61 | case "switch":
62 | // /rule switch
63 | return switchApplyRule(ctx, update, user)
64 | case "add":
65 | // /rule add