mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-05-10 17:52:44 +08:00
feat: init commit
This commit is contained in:
66
.github/workflows/build.yml
vendored
Normal file
66
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- run: npx changelogithub
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
build-matrix:
|
||||
name: Release Go Binary
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, darwin, windows]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract version from Git Ref
|
||||
id: extract_version
|
||||
run: |
|
||||
VERSION=$(echo "${{ github.ref }}" | sed 's/refs\/tags\/v//')
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Release Go Binary
|
||||
uses: wangyoucao577/go-release-action@v1
|
||||
with:
|
||||
pre_command: export
|
||||
goos: ${{ matrix.goos }}
|
||||
goarch: ${{ matrix.goarch }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
extra_files: |
|
||||
LICENSE
|
||||
README.md
|
||||
ldflags: >-
|
||||
-s -w
|
||||
-X "github.com/krau/SaveAny-Bot/common.Version=${{ env.VERSION }}"
|
||||
-X "github.com/krau/SaveAny-Bot/common.BuildTime=${{ format(github.event.repository.updated_at, 'yyyy-MM-dd HH:mm:ss') }}"
|
||||
-X "github.com/krau/SaveAny-Bot/common.GitCommit=${{ github.sha }}"
|
||||
binary_name: saveany-bot
|
||||
env:
|
||||
VERSION: ${{ env.VERSION }}
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
config.toml
|
||||
logs/
|
||||
tmp/
|
||||
data/
|
||||
downloads/
|
||||
cache/
|
||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
}
|
||||
]
|
||||
}
|
||||
21
bootstrap/init.go
Normal file
21
bootstrap/init.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/krau/SaveAny-Bot/bot"
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/dao"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
)
|
||||
|
||||
func InitAll() {
|
||||
config.Init()
|
||||
logger.InitLogger()
|
||||
logger.L.Info("Running...")
|
||||
|
||||
common.Init()
|
||||
storage.Init()
|
||||
dao.Init()
|
||||
bot.Init()
|
||||
}
|
||||
110
bot/bot.go
Normal file
110
bot/bot.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/mymmrac/telego"
|
||||
"github.com/mymmrac/telego/telegohandler"
|
||||
"github.com/mymmrac/telego/telegoutil"
|
||||
)
|
||||
|
||||
var (
|
||||
Bot *telego.Bot
|
||||
)
|
||||
|
||||
func Init() {
|
||||
logger.L.Debug("Initializing bot...")
|
||||
var err error
|
||||
Bot, err = telego.NewBot(
|
||||
config.Cfg.Telegram.Token,
|
||||
telego.WithDefaultLogger(false, true),
|
||||
telego.WithAPIServer(config.Cfg.Telegram.API),
|
||||
)
|
||||
if err != nil {
|
||||
logger.L.Fatal("Failed to create bot: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
Bot.SetMyCommands(&telego.SetMyCommandsParams{
|
||||
Commands: []telego.BotCommand{
|
||||
{Command: "start", Description: "开始使用"},
|
||||
{Command: "help", Description: "显示帮助"},
|
||||
{Command: "silent", Description: "静默模式"},
|
||||
{Command: "storage", Description: "设置默认存储位置"},
|
||||
{Command: "save", Description: "保存文件"},
|
||||
{Command: "clean", Description: "清除文件记录"},
|
||||
},
|
||||
})
|
||||
logger.L.Debug("Bot initialized")
|
||||
}
|
||||
|
||||
func Run() {
|
||||
if Bot == nil {
|
||||
Init()
|
||||
}
|
||||
logger.L.Info("Start polling...")
|
||||
updates, err := Bot.UpdatesViaLongPolling(&telego.GetUpdatesParams{
|
||||
Offset: -1,
|
||||
AllowedUpdates: []string{
|
||||
telego.MessageUpdates,
|
||||
telego.CallbackQueryUpdates,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logger.L.Fatal("Failed to start polling: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
botHandler, err := telegohandler.NewBotHandler(Bot, updates)
|
||||
if err != nil {
|
||||
logger.L.Fatal("Failed to create bot handler: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer botHandler.Stop()
|
||||
defer Bot.StopLongPolling()
|
||||
|
||||
botHandler.Use(telegohandler.PanicRecovery())
|
||||
baseGroup := botHandler.BaseGroup()
|
||||
|
||||
registerHandlers(baseGroup)
|
||||
|
||||
botHandler.Start()
|
||||
}
|
||||
|
||||
func registerHandlers(hg *telegohandler.HandlerGroup) {
|
||||
msgGroup := hg.Group(telegohandler.AnyMessage())
|
||||
msgGroup.Use(func(bot *telego.Bot, update telego.Update, next telegohandler.Handler) {
|
||||
if !slice.Contain(config.Cfg.Telegram.Admins, update.Message.From.ID) {
|
||||
bot.SendMessage(telegoutil.Message(update.Message.Chat.ChatID(), "抱歉, 该 Bot 为个人使用设计, 您可以部署自己的 AnySaveBot 实例: https://github.com/krau/SaveAny-Bot"))
|
||||
return
|
||||
}
|
||||
next(bot, update)
|
||||
})
|
||||
|
||||
msgGroup.HandleMessageCtx(Start, telegohandler.CommandEqual("start"))
|
||||
msgGroup.HandleMessageCtx(Help, telegohandler.CommandEqual("help"))
|
||||
msgGroup.HandleMessageCtx(ChangeSilentMode, telegohandler.CommandEqual("silent"))
|
||||
msgGroup.HandleMessageCtx(SetDefaultStorage, telegohandler.CommandEqual("storage"))
|
||||
msgGroup.HandleMessageCtx(SaveFile, telegohandler.CommandEqual("save"))
|
||||
msgGroup.HandleMessageCtx(CleanReceivedFile, telegohandler.CommandEqual("clean"))
|
||||
|
||||
msgGroup.HandleMessageCtx(HandleFileMessage, func(update telego.Update) bool {
|
||||
return update.Message.Document != nil || update.Message.Video != nil || update.Message.Audio != nil
|
||||
})
|
||||
|
||||
callbackGroup := hg.Group(telegohandler.AnyCallbackQueryWithMessage())
|
||||
callbackGroup.Use(func(bot *telego.Bot, update telego.Update, next telegohandler.Handler) {
|
||||
if !slice.Contain(config.Cfg.Telegram.Admins, update.CallbackQuery.From.ID) {
|
||||
bot.AnswerCallbackQuery(telegoutil.
|
||||
CallbackQuery(update.CallbackQuery.ID).
|
||||
WithText("抱歉, 该 Bot 为个人使用设计, 您可以部署自己的 SaveAnyBot 实例: https://github.com/krau/SaveAny-Bot").
|
||||
WithShowAlert().
|
||||
WithCacheTime(60))
|
||||
return
|
||||
}
|
||||
next(bot, update)
|
||||
})
|
||||
|
||||
callbackGroup.HandleCallbackQueryCtx(AddToQueue, telegohandler.CallbackDataPrefix("add"))
|
||||
}
|
||||
260
bot/handlers.go
Normal file
260
bot/handlers.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/gookit/goutil/maputil"
|
||||
"github.com/krau/SaveAny-Bot/dao"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/model"
|
||||
"github.com/krau/SaveAny-Bot/queue"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
"github.com/mymmrac/telego"
|
||||
"github.com/mymmrac/telego/telegoutil"
|
||||
)
|
||||
|
||||
func Start(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
||||
if err := dao.CreateUser(message.From.ID); err != nil {
|
||||
logger.L.Errorf("Failed to create user: %s", err)
|
||||
return
|
||||
}
|
||||
Help(ctx, bot, message)
|
||||
}
|
||||
|
||||
func Help(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
||||
helpText := `
|
||||
AnySave Bot - 转存你的 Telegram 文件
|
||||
命令:
|
||||
/start - 开始使用
|
||||
/help - 显示帮助
|
||||
/silent - 静默模式
|
||||
/storage - 设置默认存储位置
|
||||
/save - 保存文件
|
||||
/clean - 清除文件记录
|
||||
|
||||
静默模式: 开启后 Bot 直接保存到收到的文件到默认位置, 不再询问
|
||||
`
|
||||
ReplyMessage(message, helpText)
|
||||
}
|
||||
|
||||
func ChangeSilentMode(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
||||
user, err := dao.GetUserByUserID(message.From.ID)
|
||||
if err != nil {
|
||||
logger.L.Error(err)
|
||||
return
|
||||
}
|
||||
user.Silent = !user.Silent
|
||||
err = dao.UpdateUser(user)
|
||||
if err != nil {
|
||||
logger.L.Error(err)
|
||||
return
|
||||
}
|
||||
ReplyMessage(message, fmt.Sprintf("已%s静默模式", map[bool]string{true: "开启", false: "关闭"}[user.Silent]))
|
||||
}
|
||||
|
||||
func SetDefaultStorage(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
||||
if len(storage.Storages) == 0 {
|
||||
ReplyMessage(message, "当前无可用存储端, 请检查配置.")
|
||||
return
|
||||
}
|
||||
_, _, args := telegoutil.ParseCommand(message.Text)
|
||||
availableStorages := maputil.Keys(storage.Storages)
|
||||
if len(args) == 0 {
|
||||
text := EscapeMarkdown("请提供存储位置名称, 可用项:")
|
||||
for _, name := range availableStorages {
|
||||
text += fmt.Sprintf("\n`%s`", name)
|
||||
}
|
||||
text += fmt.Sprintf("\n`all`")
|
||||
bot.SendMessage(telegoutil.Message(message.Chat.ChatID(), text).WithParseMode(telego.ModeMarkdownV2))
|
||||
return
|
||||
}
|
||||
storageName := args[0]
|
||||
if !slice.Contain(availableStorages, storageName) {
|
||||
ReplyMessage(message, "参数错误")
|
||||
return
|
||||
}
|
||||
user, err := dao.GetUserByUserID(message.From.ID)
|
||||
if err != nil {
|
||||
logger.L.Error(err)
|
||||
return
|
||||
}
|
||||
user.DefaultStorage = storageName
|
||||
err = dao.UpdateUser(user)
|
||||
if err != nil {
|
||||
logger.L.Error(err)
|
||||
return
|
||||
}
|
||||
ReplyMessage(message, fmt.Sprintf("已设置默认存储位置为: %s", storageName))
|
||||
}
|
||||
|
||||
func SaveFile(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
||||
targetMessage := message.ReplyToMessage
|
||||
if targetMessage == nil {
|
||||
ReplyMessage(message, "请回复要保存的文件")
|
||||
return
|
||||
}
|
||||
if targetMessage.Document == nil && targetMessage.Video == nil && targetMessage.Audio == nil {
|
||||
ReplyMessage(message, "回复的消息不包含文件")
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, "force", true)
|
||||
HandleFileMessage(ctx, bot, *targetMessage)
|
||||
}
|
||||
|
||||
func CleanReceivedFile(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
||||
targetMessage := message.ReplyToMessage
|
||||
if targetMessage == nil {
|
||||
ReplyMessage(message, "请回复要清除记录的文件")
|
||||
return
|
||||
}
|
||||
if targetMessage.Document == nil && targetMessage.Video == nil && targetMessage.Audio == nil {
|
||||
ReplyMessage(message, "回复的消息不包含文件")
|
||||
return
|
||||
}
|
||||
var fileUniqueID string
|
||||
if targetMessage.Document != nil {
|
||||
fileUniqueID = targetMessage.Document.FileUniqueID
|
||||
} else if targetMessage.Video != nil {
|
||||
fileUniqueID = targetMessage.Video.FileUniqueID
|
||||
} else if targetMessage.Audio != nil {
|
||||
fileUniqueID = targetMessage.Audio.FileUniqueID
|
||||
}
|
||||
|
||||
if fileUniqueID == "" {
|
||||
ReplyMessage(message, "文件信息获取失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := dao.DeleteReceivedFileByFileUniqueID(fileUniqueID); err != nil {
|
||||
logger.L.Error(err)
|
||||
ReplyMessage(message, "删除记录失败")
|
||||
return
|
||||
}
|
||||
ReplyMessage(message, "记录已删除")
|
||||
}
|
||||
|
||||
func HandleFileMessage(ctx context.Context, bot *telego.Bot, message telego.Message) {
|
||||
var fileID, fileName string
|
||||
if message.Document != nil {
|
||||
fileID = message.Document.FileID
|
||||
fileName = message.Document.FileName
|
||||
} else if message.Video != nil {
|
||||
fileID = message.Video.FileID
|
||||
fileName = message.Video.FileName
|
||||
} else if message.Audio != nil {
|
||||
fileID = message.Audio.FileID
|
||||
fileName = message.Audio.FileName
|
||||
}
|
||||
|
||||
if fileID == "" || fileName == "" {
|
||||
ReplyMessage(message, "文件信息获取失败")
|
||||
return
|
||||
}
|
||||
user, err := dao.GetUserByUserID(message.From.ID)
|
||||
if err != nil {
|
||||
logger.L.Error(err)
|
||||
return
|
||||
}
|
||||
msg, err := ReplyMessage(message, "正在获取文件信息")
|
||||
if err != nil {
|
||||
logger.L.Error(err)
|
||||
return
|
||||
}
|
||||
file, err := bot.GetFile(&telego.GetFileParams{FileID: fileID})
|
||||
if err != nil {
|
||||
logger.L.Error(err)
|
||||
ReplyMessage(message, "获取文件信息失败")
|
||||
return
|
||||
}
|
||||
if ctx.Value("force") == nil {
|
||||
receivedFile, _ := dao.GetReceivedFileByFileID(file.FileID)
|
||||
if receivedFile != nil && receivedFile.Processing {
|
||||
bot.EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: message.Chat.ChatID(),
|
||||
MessageID: msg.MessageID,
|
||||
Text: "该文件或许正在处理中, 使用 /save 命令回复此文件以强制加入队列\n使用 /clean 命令回复此文件以清除对应的记录",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = dao.AddReceivedFile(&model.ReceivedFile{
|
||||
FileUniqueID: file.FileUniqueID,
|
||||
FileID: file.FileID,
|
||||
Processing: false,
|
||||
FileName: fileName,
|
||||
FilePath: file.FilePath,
|
||||
FileSize: file.FileSize,
|
||||
MediaGroupID: message.MediaGroupID,
|
||||
ChatID: message.Chat.ChatID().ID,
|
||||
MessageID: message.MessageID,
|
||||
ReplyMessageID: msg.MessageID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.L.Error(err)
|
||||
ReplyMessage(message, "创建任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
if !user.Silent {
|
||||
bot.EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: message.Chat.ChatID(),
|
||||
MessageID: msg.MessageID,
|
||||
Text: "选择要保存的位置",
|
||||
ReplyMarkup: AddTaskReplyMarkup(message.MessageID),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if user.DefaultStorage == "" {
|
||||
bot.EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: message.Chat.ChatID(),
|
||||
MessageID: msg.MessageID,
|
||||
Text: "请先使用 /storage 命令设置默认存储位置, 或者关闭静默模式",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
queue.AddTask(types.Task{
|
||||
Ctx: context.TODO(),
|
||||
FileID: file.FileID,
|
||||
Status: types.Pending,
|
||||
FileName: fileName,
|
||||
FilePath: file.FilePath,
|
||||
Storage: types.StorageType(user.DefaultStorage),
|
||||
ChatID: message.Chat.ChatID().ID,
|
||||
ReplyMessageID: msg.MessageID,
|
||||
})
|
||||
}
|
||||
|
||||
func AddToQueue(ctx context.Context, bot *telego.Bot, query telego.CallbackQuery) {
|
||||
args := strings.Split(query.Data, " ")
|
||||
messageID, _ := strconv.Atoi(args[1])
|
||||
receivedFile, err := dao.GetReceivedFileByChatAndMessageID(query.Message.GetChat().ID, messageID)
|
||||
if err != nil {
|
||||
logger.L.Error(err)
|
||||
bot.AnswerCallbackQuery(telegoutil.CallbackQuery(query.ID).WithShowAlert().WithText("获取文件信息失败").WithCacheTime(5))
|
||||
return
|
||||
}
|
||||
queue.AddTask(types.Task{
|
||||
Ctx: context.TODO(),
|
||||
FileID: receivedFile.FileID,
|
||||
Status: types.Pending,
|
||||
FileName: receivedFile.FileName,
|
||||
FilePath: receivedFile.FilePath,
|
||||
Storage: types.StorageType(args[2]),
|
||||
ChatID: receivedFile.ChatID,
|
||||
ReplyMessageID: receivedFile.ReplyMessageID,
|
||||
})
|
||||
bot.EditMessageText(&telego.EditMessageTextParams{
|
||||
Text: fmt.Sprintf("已添加到队列, 当前排队中的任务数: %d", queue.Len()),
|
||||
MessageID: query.Message.GetMessageID(),
|
||||
ChatID: telegoutil.ID(receivedFile.ChatID),
|
||||
})
|
||||
}
|
||||
1
bot/middleware.go
Normal file
1
bot/middleware.go
Normal file
@@ -0,0 +1 @@
|
||||
package bot
|
||||
49
bot/utils.go
Normal file
49
bot/utils.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/mymmrac/telego"
|
||||
"github.com/mymmrac/telego/telegoutil"
|
||||
)
|
||||
|
||||
func FileDownloadURL(filePath string) string {
|
||||
return Bot.FileDownloadURL(filePath)
|
||||
}
|
||||
|
||||
func ReplyMessage(replyTo telego.Message, format string, args ...any) (*telego.Message, error) {
|
||||
return Bot.SendMessage(telegoutil.Messagef(replyTo.Chat.ChatID(), format, args...).
|
||||
WithReplyParameters(&telego.ReplyParameters{
|
||||
MessageID: replyTo.MessageID,
|
||||
}))
|
||||
}
|
||||
|
||||
func AddTaskReplyMarkup(messageID int) *telego.InlineKeyboardMarkup {
|
||||
storageButtons := make([]telego.InlineKeyboardButton, 0)
|
||||
if config.Cfg.Storage.Local.Enable {
|
||||
storageButtons = append(storageButtons, telegoutil.InlineKeyboardButton("服务器磁盘").
|
||||
WithCallbackData(fmt.Sprintf("add %d local", messageID)))
|
||||
}
|
||||
if config.Cfg.Storage.Alist.Enable {
|
||||
storageButtons = append(storageButtons, telegoutil.InlineKeyboardButton("Alist").
|
||||
WithCallbackData(fmt.Sprintf("add %d alist", messageID)))
|
||||
}
|
||||
|
||||
if len(storageButtons) > 1 {
|
||||
return telegoutil.InlineKeyboard(storageButtons, []telego.InlineKeyboardButton{
|
||||
telegoutil.InlineKeyboardButton("全部").WithCallbackData(fmt.Sprintf("add %d all", messageID)),
|
||||
})
|
||||
}
|
||||
if len(storageButtons) == 1 {
|
||||
return telegoutil.InlineKeyboard(storageButtons)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var markdownRe = regexp.MustCompile("([" + regexp.QuoteMeta(`\_*[]()~`+"`"+`>#+-=|{}.!`) + "])")
|
||||
|
||||
func EscapeMarkdown(text string) string {
|
||||
return markdownRe.ReplaceAllString(text, "\\$1")
|
||||
}
|
||||
17
cmd/root.go
Normal file
17
cmd/root.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "saveany-bot",
|
||||
Short: "saveany-bot",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run()
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
rootCmd.Execute()
|
||||
}
|
||||
13
cmd/run.go
Normal file
13
cmd/run.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/krau/SaveAny-Bot/bootstrap"
|
||||
"github.com/krau/SaveAny-Bot/bot"
|
||||
"github.com/krau/SaveAny-Bot/core"
|
||||
)
|
||||
|
||||
func Run() {
|
||||
bootstrap.InitAll()
|
||||
go core.Run()
|
||||
bot.Run()
|
||||
}
|
||||
18
cmd/version.go
Normal file
18
cmd/version.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var VersionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Aliases: []string{"v"},
|
||||
Short: "Print the version number of saveany-bot",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("saveany-bot version: %s %s/%s\nBuildTime: %s, Commit: %s\n", common.Version, runtime.GOOS, runtime.GOARCH, common.BuildTime, common.GitCommit)
|
||||
},
|
||||
}
|
||||
5
common/common.go
Normal file
5
common/common.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package common
|
||||
|
||||
func Init() {
|
||||
initClient()
|
||||
}
|
||||
71
common/os.go
Normal file
71
common/os.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
)
|
||||
|
||||
// 创建文件, 自动创建目录
|
||||
func MkFile(path string, data []byte) error {
|
||||
err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, os.ModePerm)
|
||||
}
|
||||
|
||||
func FileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// 删除文件, 并清理空目录. 如果文件不存在则返回 nil
|
||||
func PurgeFile(path string) error {
|
||||
if err := os.Remove(path); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return RemoveEmptyDirectories(filepath.Dir(path))
|
||||
}
|
||||
|
||||
// 递归删除空目录
|
||||
func RemoveEmptyDirectories(dirPath string) error {
|
||||
entries, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
err := os.Remove(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return RemoveEmptyDirectories(filepath.Dir(dirPath))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 在指定时间后删除和清理文件 (定时器)
|
||||
func PurgeFileAfter(path string, td time.Duration) {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to create timer for %s: %s", path, err)
|
||||
return
|
||||
}
|
||||
logger.L.Debugf("Purge file after %s: %s", td, path)
|
||||
time.AfterFunc(td, func() {
|
||||
PurgeFile(path)
|
||||
})
|
||||
}
|
||||
|
||||
func MkCache(path string, data []byte, td time.Duration) {
|
||||
if err := MkFile(path, data); err != nil {
|
||||
logger.L.Errorf("failed to save cache file: %s", err)
|
||||
} else {
|
||||
go PurgeFileAfter(path, td)
|
||||
}
|
||||
}
|
||||
18
common/req.go
Normal file
18
common/req.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
)
|
||||
|
||||
var ReqClient *req.Client
|
||||
|
||||
func initClient() {
|
||||
ReqClient = req.NewClient().SetOutputDirectory(config.Cfg.Temp.BasePath)
|
||||
}
|
||||
|
||||
func GetDownloadedFilePath(filename string) string {
|
||||
return filepath.Join(config.Cfg.Temp.BasePath, filename)
|
||||
}
|
||||
7
common/version.go
Normal file
7
common/version.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package common
|
||||
|
||||
var (
|
||||
Version string = "dev"
|
||||
BuildTime string = "unknown"
|
||||
GitCommit string = "unknown"
|
||||
)
|
||||
89
config/viper.go
Normal file
89
config/viper.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Temp tempConfig `toml:"temp" mapstructure:"temp"`
|
||||
Log logConfig `toml:"log" mapstructure:"log"`
|
||||
DB dbConfig `toml:"db" mapstructure:"db"`
|
||||
Telegram telegramConfig `toml:"telegram" mapstructure:"telegram"`
|
||||
Storage storageConfig `toml:"storage" mapstructure:"storage"`
|
||||
}
|
||||
|
||||
type tempConfig struct {
|
||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
||||
CacheTTL int64 `toml:"cache_ttl" mapstructure:"cache_ttl"`
|
||||
}
|
||||
|
||||
type logConfig struct {
|
||||
Level string `toml:"level" mapstructure:"level"`
|
||||
File string `toml:"file" mapstructure:"file"`
|
||||
BackupCount uint `toml:"backup_count" mapstructure:"backup_count"`
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
Path string `toml:"path" mapstructure:"path"`
|
||||
}
|
||||
|
||||
type telegramConfig struct {
|
||||
Token string `toml:"token" mapstructure:"token"`
|
||||
API string `toml:"api" mapstructure:"api"`
|
||||
Admins []int64 `toml:"admins" mapstructure:"admins"`
|
||||
}
|
||||
|
||||
type storageConfig struct {
|
||||
Alist alistConfig `toml:"alist" mapstructure:"alist"`
|
||||
Local localConfig `toml:"local" mapstructure:"local"`
|
||||
}
|
||||
|
||||
type alistConfig struct {
|
||||
Enable bool `toml:"enable" mapstructure:"enable"`
|
||||
URL string `toml:"url" mapstructure:"url"`
|
||||
Username string `toml:"username" mapstructure:"username"`
|
||||
Password string `toml:"password" mapstructure:"password"`
|
||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
||||
TokenExp int64 `toml:"token_exp" mapstructure:"token_exp"`
|
||||
}
|
||||
|
||||
type localConfig struct {
|
||||
Enable bool `toml:"enable" mapstructure:"enable"`
|
||||
BasePath string `toml:"base_path" mapstructure:"base_path"`
|
||||
}
|
||||
|
||||
var Cfg *Config
|
||||
|
||||
func Init() {
|
||||
viper.SetConfigName("config")
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigType("toml")
|
||||
|
||||
viper.SetDefault("temp.base_path", "cache/")
|
||||
viper.SetDefault("temp.cache_ttl", 3600)
|
||||
|
||||
viper.SetDefault("log.level", "INFO")
|
||||
viper.SetDefault("log.file", "logs/anysave.log")
|
||||
viper.SetDefault("log.backup_count", 7)
|
||||
|
||||
viper.SetDefault("db.path", "data/anysave.db")
|
||||
|
||||
viper.SetDefault("telegram.api", "https://api.telegram.org")
|
||||
|
||||
viper.SetDefault("storage.alist.base_path", "/")
|
||||
viper.SetDefault("storage.alist.token_exp", 3600)
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
fmt.Println("Error reading config file, ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
Cfg = &Config{}
|
||||
if err := viper.Unmarshal(Cfg); err != nil {
|
||||
fmt.Println("Error unmarshalling config file, ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
135
core/core.go
Normal file
135
core/core.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/krau/SaveAny-Bot/bot"
|
||||
"github.com/krau/SaveAny-Bot/common"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/dao"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/queue"
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
"github.com/mymmrac/telego"
|
||||
"github.com/mymmrac/telego/telegoutil"
|
||||
)
|
||||
|
||||
func processPendingTask(task types.Task) error {
|
||||
fileRecord, err := dao.GetReceivedFileByFileID(task.FileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileRecord.Processing = true
|
||||
if err := dao.UpdateReceivedFile(fileRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = common.ReqClient.R().SetContext(task.Ctx).SetOutputFile(task.FileName).SetDownloadCallbackWithInterval(func(info req.DownloadInfo) {
|
||||
if info.Response == nil || info.Response.Response == nil || info.Response.Response.StatusCode != 200 {
|
||||
return
|
||||
}
|
||||
process := fmt.Sprintf("%.2f%%", float64(info.DownloadedSize)/float64(info.Response.ContentLength)*100.0)
|
||||
bot.Bot.EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: telegoutil.ID(task.ChatID),
|
||||
MessageID: task.ReplyMessageID,
|
||||
Text: "正在下载文件: " + process,
|
||||
})
|
||||
}, time.Second*time.Duration(3*func() int {
|
||||
if queue.Len() > 0 {
|
||||
return queue.Len()
|
||||
}
|
||||
return 1
|
||||
}())).Get(bot.FileDownloadURL(task.FilePath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bot.Bot.EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: telegoutil.ID(task.ChatID),
|
||||
MessageID: task.ReplyMessageID,
|
||||
Text: "下载完成, 正在转存...",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
if config.Cfg.Temp.CacheTTL > 0 {
|
||||
common.PurgeFileAfter(common.GetDownloadedFilePath(task.FileName),
|
||||
time.Duration(config.Cfg.Temp.CacheTTL)*time.Second)
|
||||
} else {
|
||||
if err := common.PurgeFile(common.GetDownloadedFilePath(task.FileName)); err != nil {
|
||||
logger.L.Errorf("Failed to purge file: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if task.StoragePath == "" {
|
||||
task.StoragePath = task.FileName
|
||||
}
|
||||
|
||||
if err := storage.Save(task.Storage, task.Ctx, common.GetDownloadedFilePath(task.FileName), task.StoragePath); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
|
||||
for {
|
||||
semaphore <- struct{}{}
|
||||
task := queue.GetTask()
|
||||
logger.L.Debugf("Got task: %s", task.FileName)
|
||||
|
||||
switch task.Status {
|
||||
case types.Pending:
|
||||
logger.L.Infof("Processing task: %s", task.String())
|
||||
if err := processPendingTask(task); err != nil {
|
||||
logger.L.Errorf("Failed to do task: %s", err)
|
||||
task.Error = err
|
||||
if errors.Is(err, context.Canceled) {
|
||||
task.Status = types.Canceled
|
||||
} else {
|
||||
task.Status = types.Failed
|
||||
}
|
||||
} else {
|
||||
task.Status = types.Succeeded
|
||||
}
|
||||
queue.AddTask(task)
|
||||
case types.Succeeded:
|
||||
logger.L.Infof("Task succeeded: %s", task.String())
|
||||
bot.Bot.EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: telegoutil.ID(task.ChatID),
|
||||
MessageID: task.ReplyMessageID,
|
||||
Text: "文件转存完成",
|
||||
})
|
||||
if err := dao.DeleteReceivedFileByFileID(task.FileID); err != nil {
|
||||
logger.L.Errorf("Failed to delete received file: %s", err)
|
||||
}
|
||||
case types.Failed:
|
||||
logger.L.Errorf("Task failed: %s", task.String())
|
||||
bot.Bot.EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: telegoutil.ID(task.ChatID),
|
||||
MessageID: task.ReplyMessageID,
|
||||
Text: "文件转存失败:" + "\n" + task.Error.Error(),
|
||||
})
|
||||
if err := dao.DeleteReceivedFileByFileID(task.FileID); err != nil {
|
||||
logger.L.Errorf("Failed to delete received file: %s", err)
|
||||
}
|
||||
case types.Canceled:
|
||||
logger.L.Infof("Task canceled: %s", task.String())
|
||||
default:
|
||||
logger.L.Errorf("Unknown task status: %s", task.Status)
|
||||
}
|
||||
<-semaphore
|
||||
logger.L.Debugf("Task done: %s", task.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
func Run() {
|
||||
logger.L.Info("Start processing tasks...")
|
||||
semaphore := make(chan struct{}, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
go worker(queue.Queue, semaphore)
|
||||
}
|
||||
}
|
||||
33
dao/db.go
Normal file
33
dao/db.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
func Init() {
|
||||
if err := os.MkdirAll(filepath.Dir(config.Cfg.DB.Path), 755); err != nil {
|
||||
logger.L.Fatal("Failed to create data directory: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var err error
|
||||
db, err = gorm.Open(sqlite.Open(config.Cfg.DB.Path), &gorm.Config{})
|
||||
if err != nil {
|
||||
logger.L.Fatal("Failed to open database: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.L.Debug("Database connected")
|
||||
db.AutoMigrate(&model.ReceivedFile{}, &model.User{})
|
||||
|
||||
for _, admin := range config.Cfg.Telegram.Admins {
|
||||
CreateUser(int64(admin))
|
||||
}
|
||||
}
|
||||
55
dao/file.go
Normal file
55
dao/file.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package dao
|
||||
|
||||
import "github.com/krau/SaveAny-Bot/model"
|
||||
|
||||
func AddReceivedFile(receivedFile *model.ReceivedFile) error {
|
||||
return db.Create(receivedFile).Error
|
||||
}
|
||||
|
||||
func GetReceivedFileByFileID(fileID string) (*model.ReceivedFile, error) {
|
||||
var receivedFile model.ReceivedFile
|
||||
err := db.Where("file_id = ?", fileID).First(&receivedFile).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &receivedFile, nil
|
||||
}
|
||||
|
||||
func GetReceivedFileByFileUniqueID(fileUniqueID string) (*model.ReceivedFile, error) {
|
||||
var receivedFile model.ReceivedFile
|
||||
err := db.Where("file_unique_id = ?", fileUniqueID).First(&receivedFile).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &receivedFile, nil
|
||||
}
|
||||
|
||||
func GetReceivedFileByChatAndMessageID(chatID int64, messageID int) (*model.ReceivedFile, error) {
|
||||
var receivedFile model.ReceivedFile
|
||||
err := db.Where("chat_id = ? AND message_id = ?", chatID, messageID).First(&receivedFile).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &receivedFile, nil
|
||||
}
|
||||
|
||||
func GetReceivedFilesByMediaGroupID(mediaGroupID string) ([]model.ReceivedFile, error) {
|
||||
var receivedFiles []model.ReceivedFile
|
||||
err := db.Where("media_group_id = ?", mediaGroupID).Find(&receivedFiles).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return receivedFiles, nil
|
||||
}
|
||||
|
||||
func UpdateReceivedFile(receivedFile *model.ReceivedFile) error {
|
||||
return db.Save(receivedFile).Error
|
||||
}
|
||||
|
||||
func DeleteReceivedFileByFileID(fileID string) error {
|
||||
return db.Where("file_id = ?", fileID).Delete(&model.ReceivedFile{}).Error
|
||||
}
|
||||
|
||||
func DeleteReceivedFileByFileUniqueID(fileUniqueID string) error {
|
||||
return db.Where("file_unique_id = ?", fileUniqueID).Delete(&model.ReceivedFile{}).Error
|
||||
}
|
||||
22
dao/user.go
Normal file
22
dao/user.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"github.com/krau/SaveAny-Bot/model"
|
||||
)
|
||||
|
||||
func CreateUser(userID int64) error {
|
||||
if _, err := GetUserByUserID(userID); err == nil {
|
||||
return nil
|
||||
}
|
||||
return db.Create(&model.User{UserID: userID}).Error
|
||||
}
|
||||
|
||||
func GetUserByUserID(userID int64) (*model.User, error) {
|
||||
var user model.User
|
||||
err := db.Where("user_id = ?", userID).First(&user).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func UpdateUser(user *model.User) error {
|
||||
return db.Save(user).Error
|
||||
}
|
||||
85
go.mod
Normal file
85
go.mod
Normal file
@@ -0,0 +1,85 @@
|
||||
module github.com/krau/SaveAny-Bot
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
github.com/gookit/slog v0.5.6
|
||||
github.com/imroc/req/v3 v3.46.1
|
||||
github.com/mymmrac/telego v0.31.3
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cloudflare/circl v1.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20241009165004-a3522334989c // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.47.0 // indirect
|
||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/duke-git/lancet/v2 v2.3.3
|
||||
github.com/fasthttp/router v1.5.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gookit/goutil v0.6.15
|
||||
github.com/gookit/gsr v0.1.0 // indirect
|
||||
github.com/grbit/go-json v0.11.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.10 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.55.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.6.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
201
go.sum
Normal file
201
go.sum
Normal file
@@ -0,0 +1,201 @@
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=
|
||||
github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
|
||||
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/duke-git/lancet/v2 v2.3.3 h1:OhqzNzkbJBS9ZlWLo/C7g+WSAOAAyNj7p9CAiEHurUc=
|
||||
github.com/duke-git/lancet/v2 v2.3.3/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/fasthttp/router v1.5.2 h1:ckJCCdV7hWkkrMeId3WfEhz+4Gyyf6QPwxi/RHIMZ6I=
|
||||
github.com/fasthttp/router v1.5.2/go.mod h1:C8EY53ozOwpONyevc/V7Gr8pqnEjwnkFFqPo1alAGs0=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20241009165004-a3522334989c h1:NDovD0SMpBYXlE1zJmS1q55vWB/fUQBcPAqAboZSccA=
|
||||
github.com/google/pprof v0.0.0-20241009165004-a3522334989c/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
|
||||
github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
|
||||
github.com/gookit/gsr v0.1.0 h1:0gadWaYGU4phMs0bma38t+Do5OZowRMEVlHv31p0Zig=
|
||||
github.com/gookit/gsr v0.1.0/go.mod h1:7wv4Y4WCnil8+DlDYHBjidzrEzfHhXEoFjEA0pPPWpI=
|
||||
github.com/gookit/slog v0.5.6 h1:fmh+7bfOK8CjidMCwE+M3S8G766oHJpT/1qdmXGALCI=
|
||||
github.com/gookit/slog v0.5.6/go.mod h1:RfIwzoaQ8wZbKdcqG7+3EzbkMqcp2TUn3mcaSZAw2EQ=
|
||||
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
|
||||
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/imroc/req/v3 v3.46.1 h1:oahr2hBTb3AaFI4P6jkN0Elj2ZVKJcdQ/IjWqeIKjvc=
|
||||
github.com/imroc/req/v3 v3.46.1/go.mod h1:weam9gmyb00QnOtu6HXSnk44dNFkIUQb5QdMx13FeUU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
|
||||
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/mymmrac/telego v0.31.3 h1:yZlD+dm+1W6p3OmCG8K+MbS02Y6paUgwPnqfZN3RWQQ=
|
||||
github.com/mymmrac/telego v0.31.3/go.mod h1:coOoqXVmjFnwBlzusjfEezbQ7RH9wQnDowJdMm+bnEo=
|
||||
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
|
||||
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y=
|
||||
github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E=
|
||||
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
||||
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
|
||||
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
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.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
|
||||
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
40
logger/logger.go
Normal file
40
logger/logger.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
|
||||
"github.com/gookit/slog"
|
||||
"github.com/gookit/slog/handler"
|
||||
"github.com/gookit/slog/rotatefile"
|
||||
)
|
||||
|
||||
var L *slog.Logger
|
||||
|
||||
func InitLogger() {
|
||||
if L != nil {
|
||||
return
|
||||
}
|
||||
slog.DefaultChannelName = "SaveAnyBot"
|
||||
L = slog.New()
|
||||
logLevel := slog.LevelByName(config.Cfg.Log.Level)
|
||||
logFilePath := config.Cfg.Log.File
|
||||
logBackupNum := config.Cfg.Log.BackupCount
|
||||
var logLevels []slog.Level
|
||||
for _, level := range slog.AllLevels {
|
||||
if level <= logLevel {
|
||||
logLevels = append(logLevels, level)
|
||||
}
|
||||
}
|
||||
consoleH := handler.NewConsoleHandler(logLevels)
|
||||
fileH, err := handler.NewTimeRotateFile(
|
||||
logFilePath,
|
||||
rotatefile.EveryDay,
|
||||
handler.WithLogLevels(slog.AllLevels),
|
||||
handler.WithBackupNum(logBackupNum),
|
||||
handler.WithBuffSize(0),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
L.AddHandlers(consoleH, fileH)
|
||||
}
|
||||
7
main.go
Normal file
7
main.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "github.com/krau/SaveAny-Bot/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Run()
|
||||
}
|
||||
28
model/model.go
Normal file
28
model/model.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ReceivedFile struct {
|
||||
gorm.Model
|
||||
// FileUniqueID 和 FileID 在数据库中均不具有唯一性
|
||||
// 如需确定唯一一行, 使用 ChatID + MessageID
|
||||
FileUniqueID string `gorm:"index"`
|
||||
FileID string `gorm:"index"`
|
||||
Processing bool
|
||||
FileName string
|
||||
FilePath string
|
||||
FileSize int64
|
||||
MediaGroupID string
|
||||
ChatID int64
|
||||
MessageID int
|
||||
ReplyMessageID int
|
||||
}
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
UserID int64 `gorm:"uniqueIndex"`
|
||||
Silent bool
|
||||
DefaultStorage string
|
||||
}
|
||||
66
queue/queue.go
Normal file
66
queue/queue.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
type TaskQueue struct {
|
||||
list *list.List
|
||||
cond *sync.Cond
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
|
||||
func (q *TaskQueue) AddTask(task types.Task) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
q.list.PushBack(task)
|
||||
q.cond.Signal()
|
||||
}
|
||||
|
||||
func (q *TaskQueue) GetTask() types.Task {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
for q.list.Len() == 0 {
|
||||
q.cond.Wait()
|
||||
}
|
||||
e := q.list.Front()
|
||||
task := e.Value.(types.Task)
|
||||
q.list.Remove(e)
|
||||
return task
|
||||
}
|
||||
|
||||
func (q *TaskQueue) Len() int {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
return q.list.Len()
|
||||
}
|
||||
|
||||
var Queue *TaskQueue
|
||||
|
||||
func init() {
|
||||
Queue = NewQueue()
|
||||
}
|
||||
|
||||
func NewQueue() *TaskQueue {
|
||||
m := &sync.Mutex{}
|
||||
return &TaskQueue{
|
||||
list: list.New(),
|
||||
cond: sync.NewCond(m),
|
||||
mutex: m,
|
||||
}
|
||||
}
|
||||
|
||||
func AddTask(task types.Task) {
|
||||
Queue.AddTask(task)
|
||||
}
|
||||
|
||||
func GetTask() types.Task {
|
||||
return Queue.GetTask()
|
||||
}
|
||||
|
||||
func Len() int {
|
||||
return Queue.Len()
|
||||
}
|
||||
109
storage/alist/alist.go
Normal file
109
storage/alist/alist.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package alist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
)
|
||||
|
||||
type Alist struct{}
|
||||
|
||||
var (
|
||||
basePath string
|
||||
baseUrl string
|
||||
reqClient *req.Client
|
||||
loginReq *loginRequset
|
||||
|
||||
ErrAlistLoginFailed = errors.New("failed to login to Alist")
|
||||
)
|
||||
|
||||
type loginRequset struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type loginResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func getToken() (string, error) {
|
||||
resp, err := reqClient.R().SetBodyJsonMarshal(loginReq).Post("/api/auth/login")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var loginResp loginResponse
|
||||
if err := json.Unmarshal(resp.Bytes(), &loginResp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if loginResp.Code != http.StatusOK {
|
||||
return "", fmt.Errorf("%w: %s", ErrAlistLoginFailed, loginResp.Message)
|
||||
}
|
||||
return loginResp.Data.Token, nil
|
||||
}
|
||||
|
||||
func refreshToken(client *req.Client) {
|
||||
for {
|
||||
time.Sleep(time.Duration(config.Cfg.Storage.Alist.TokenExp) * time.Second)
|
||||
token, err := getToken()
|
||||
if err != nil {
|
||||
logger.L.Errorf("Failed to refresh jwt token: %v", err)
|
||||
continue
|
||||
}
|
||||
client.SetCommonHeader("Authorization", token)
|
||||
logger.L.Info("Refreshed Alist jwt token")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Alist) Init() {
|
||||
basePath = config.Cfg.Storage.Alist.BasePath
|
||||
baseUrl = config.Cfg.Storage.Alist.URL
|
||||
reqClient = req.C().SetTLSHandshakeTimeout(time.Second * 10).SetBaseURL(baseUrl)
|
||||
loginReq = &loginRequset{
|
||||
Username: config.Cfg.Storage.Alist.Username,
|
||||
Password: config.Cfg.Storage.Alist.Password,
|
||||
}
|
||||
token, err := getToken()
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to login to Alist: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.L.Debug("Logged in to Alist")
|
||||
reqClient.SetCommonHeader("Authorization", token)
|
||||
go refreshToken(reqClient)
|
||||
}
|
||||
|
||||
func (a *Alist) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
storagePath = path.Join(basePath, storagePath)
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := reqClient.R().
|
||||
SetContext(ctx).
|
||||
SetBody(file).
|
||||
SetHeaders(map[string]string{
|
||||
"File-Path": url.PathEscape(storagePath),
|
||||
"As-Task": "true",
|
||||
}).Put("/api/fs/put")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to save file to Alist: %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
25
storage/local/local.go
Normal file
25
storage/local/local.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/duke-git/lancet/v2/fileutil"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
)
|
||||
|
||||
type Local struct{}
|
||||
|
||||
func (l *Local) Init() {
|
||||
err := os.MkdirAll(config.Cfg.Storage.Local.BasePath, os.ModePerm)
|
||||
if err != nil {
|
||||
logger.L.Fatalf("Failed to create local storage directory: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Local) Save(ctx context.Context, filePath, storagePath string) error {
|
||||
return fileutil.CopyFile(filePath, filepath.Join(config.Cfg.Storage.Local.BasePath, storagePath))
|
||||
}
|
||||
59
storage/storage.go
Normal file
59
storage/storage.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/logger"
|
||||
"github.com/krau/SaveAny-Bot/storage/alist"
|
||||
"github.com/krau/SaveAny-Bot/storage/local"
|
||||
"github.com/krau/SaveAny-Bot/types"
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
Init()
|
||||
Save(cttx context.Context, filePath, storagePath string) error
|
||||
}
|
||||
|
||||
var Storages = make(map[types.StorageType]Storage)
|
||||
|
||||
func Init() {
|
||||
logger.L.Debug("Initializing storage...")
|
||||
if config.Cfg.Storage.Alist.Enable {
|
||||
Storages[types.Alist] = new(alist.Alist)
|
||||
Storages[types.Alist].Init()
|
||||
}
|
||||
if config.Cfg.Storage.Local.Enable {
|
||||
Storages[types.Local] = new(local.Local)
|
||||
Storages[types.Local].Init()
|
||||
}
|
||||
|
||||
logger.L.Debug("Storage initialized")
|
||||
}
|
||||
|
||||
func Save(storageType types.StorageType, ctx context.Context, filePath, storagePath string) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if storageType != types.StorageAll {
|
||||
return Storages[storageType].Save(ctx, filePath, storagePath)
|
||||
}
|
||||
errs := make([]error, 0)
|
||||
var wg sync.WaitGroup
|
||||
for _, storage := range Storages {
|
||||
wg.Add(1)
|
||||
go func(storage Storage) {
|
||||
defer wg.Done()
|
||||
if err := storage.Save(ctx, filePath, storagePath); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}(storage)
|
||||
}
|
||||
wg.Wait()
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
41
types/types.go
Normal file
41
types/types.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package types
|
||||
|
||||
import "context"
|
||||
|
||||
type TaskStatus string
|
||||
|
||||
var (
|
||||
Pending TaskStatus = "pending"
|
||||
Succeeded TaskStatus = "succeeded"
|
||||
Failed TaskStatus = "failed"
|
||||
Canceled TaskStatus = "canceled"
|
||||
)
|
||||
|
||||
type StorageType string
|
||||
|
||||
var (
|
||||
StorageAll StorageType = "all"
|
||||
Local StorageType = "local"
|
||||
Alist StorageType = "alist"
|
||||
)
|
||||
|
||||
var StorageTypes = []StorageType{Local, Alist, StorageAll}
|
||||
|
||||
type Task struct {
|
||||
Ctx context.Context
|
||||
FileID string
|
||||
Error error
|
||||
Status TaskStatus
|
||||
FilePath string // telegram File object's FilePath
|
||||
FileName string
|
||||
Storage StorageType
|
||||
StoragePath string
|
||||
|
||||
// For track progress
|
||||
ChatID int64
|
||||
ReplyMessageID int
|
||||
}
|
||||
|
||||
func (t *Task) String() string {
|
||||
return t.FileName
|
||||
}
|
||||
Reference in New Issue
Block a user