mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-08-27 03:10:06 +08:00
Compare commits
58 Commits
v0.60.4
...
fix/qualit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
942022969c | ||
|
|
ab89310325 | ||
|
|
b27bb734be | ||
|
|
fc0c8975a0 | ||
|
|
524a259ae4 | ||
|
|
e8257c46d2 | ||
|
|
6a11b8ac8d | ||
|
|
47d941d851 | ||
|
|
7e502442c5 | ||
|
|
a9425cb5ea | ||
|
|
6ad2a2c884 | ||
|
|
0d42c9f23d | ||
|
|
4c8f35ae80 | ||
|
|
0a67b8bb57 | ||
|
|
196bb9941f | ||
|
|
973f3499a9 | ||
|
|
d9cb5f9cfc | ||
|
|
ccb702148e | ||
|
|
85b1f35dc5 | ||
|
|
9f93a95258 | ||
|
|
189bf9c736 | ||
|
|
534ed7a7c2 | ||
|
|
1d4997ba2d | ||
|
|
f7e532ca7e | ||
|
|
0957d93da3 | ||
|
|
324d8c100f | ||
|
|
4cb23f04b6 | ||
|
|
a92ca75c4f | ||
|
|
4e99b4bdc9 | ||
|
|
389be59371 | ||
|
|
0d49ae94af | ||
|
|
48e739f06d | ||
|
|
b4fbbae068 | ||
|
|
29be3ee90c | ||
|
|
56660a7705 | ||
|
|
0dfb6af153 | ||
|
|
63f53fac7f | ||
|
|
607ec8aced | ||
|
|
cd9886d710 | ||
|
|
3f7f50133e | ||
|
|
c7911cc208 | ||
|
|
b31d628c19 | ||
|
|
60fd2e04d9 | ||
|
|
b72dd67be9 | ||
|
|
aa25eb1510 | ||
|
|
ba0deababc | ||
|
|
d1b2dbfe5f | ||
|
|
bd7da7c31e | ||
|
|
1981d8662b | ||
|
|
51fcd17922 | ||
|
|
610f586d45 | ||
|
|
6bc12d6feb | ||
|
|
ad41aec22c | ||
|
|
4389bfe046 | ||
|
|
832eb27d4f | ||
|
|
b6e981282d | ||
|
|
9ed5b5f962 | ||
|
|
4527022853 |
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -196,6 +195,6 @@ func (t *TaskProgressInfo) Emit(e taskevent.Event) {
|
||||
|
||||
if notify {
|
||||
payload := CreateWebhookPayload(t.TaskID, t.Type, t.Status, t.Storage, t.Path, e.Err)
|
||||
SendWebhook(context.Background(), payload)
|
||||
SendWebhook(nil, payload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/celestix/gotgproto/dispatcher"
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/dirutil"
|
||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
@@ -15,23 +12,10 @@ import (
|
||||
"github.com/krau/SaveAny-Bot/storage"
|
||||
)
|
||||
|
||||
// responsibleUserID returns the sender's ID. Callback queries carry it
|
||||
// natively; message updates resolve it through the entity map.
|
||||
func responsibleUserID(u *ext.Update) int64 {
|
||||
if u.CallbackQuery != nil {
|
||||
return u.CallbackQuery.GetUserID()
|
||||
}
|
||||
return u.GetUserChat().GetID()
|
||||
}
|
||||
|
||||
func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
||||
userID := responsibleUserID(update)
|
||||
userID := update.GetUserChat().GetID()
|
||||
if !slice.Contain(config.C().GetUsersID(), userID) {
|
||||
if cbq := update.CallbackQuery; cbq != nil {
|
||||
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(cbq.GetQueryID(), i18n.T(i18nk.BotMsgCommonErrorNoPermission, nil)))
|
||||
} else {
|
||||
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorNoPermission, nil)), nil)
|
||||
}
|
||||
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgCommonErrorNoPermission, nil)), nil)
|
||||
return dispatcher.EndGroups
|
||||
}
|
||||
|
||||
@@ -39,12 +23,10 @@ func checkPermission(ctx *ext.Context, update *ext.Update) error {
|
||||
}
|
||||
|
||||
// withPermission wraps a callback handler with the same whitelist check used
|
||||
// for message handlers (checkPermission). ContinueGroups is the dispatcher's
|
||||
// success sentinel, not an error: only real failures and EndGroups stop the
|
||||
// chain before the wrapped handler runs.
|
||||
// for message handlers (checkPermission).
|
||||
func withPermission(handler func(*ext.Context, *ext.Update) error) func(*ext.Context, *ext.Update) error {
|
||||
return func(ctx *ext.Context, update *ext.Update) error {
|
||||
if err := checkPermission(ctx, update); err != nil && !errors.Is(err, dispatcher.ContinueGroups) {
|
||||
if err := checkPermission(ctx, update); err != nil {
|
||||
return err
|
||||
}
|
||||
return handler(ctx, update)
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
"github.com/celestix/gotgproto/types"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
)
|
||||
|
||||
// Regression: callback queries usually arrive as updateShort without entity
|
||||
// maps, so resolving the sender through the entity map yields ID 0 and every
|
||||
// click was denied by the whitelist check. Callback updates must use the
|
||||
// native UserID field.
|
||||
func TestResponsibleUserID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
update *ext.Update
|
||||
want int64
|
||||
}{
|
||||
{
|
||||
name: "callback query uses native user id",
|
||||
update: &ext.Update{CallbackQuery: &tg.UpdateBotCallbackQuery{UserID: 42}},
|
||||
want: 42,
|
||||
},
|
||||
{
|
||||
name: "message resolves through entity map",
|
||||
update: &ext.Update{
|
||||
EffectiveMessage: &types.Message{Message: &tg.Message{PeerID: &tg.PeerUser{UserID: 7}}},
|
||||
Entities: &tg.Entities{Users: map[int64]*tg.User{7: {ID: 7}}},
|
||||
},
|
||||
want: 7,
|
||||
},
|
||||
{
|
||||
name: "callback query ignores entity map",
|
||||
update: &ext.Update{
|
||||
CallbackQuery: &tg.UpdateBotCallbackQuery{UserID: 9},
|
||||
Entities: &tg.Entities{Users: map[int64]*tg.User{8: {ID: 8}}},
|
||||
},
|
||||
want: 9,
|
||||
},
|
||||
{
|
||||
name: "unresolvable update yields zero",
|
||||
update: &ext.Update{},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := responsibleUserID(tt.update); got != tt.want {
|
||||
t.Fatalf("responsibleUserID() = %d, want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Regression: withPermission must treat ContinueGroups (the dispatcher's
|
||||
// success sentinel) as a pass and invoke the wrapped handler. v0.60.1 treated
|
||||
// it as an error, so every permitted callback was swallowed before the real
|
||||
// handler ran.
|
||||
func TestWithPermissionInvokesHandler(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.toml")
|
||||
if err := os.WriteFile(path, []byte("workers = 2\n\n[[users]]\nid = 42\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := config.Init(t.Context(), path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
update := &ext.Update{CallbackQuery: &tg.UpdateBotCallbackQuery{UserID: 42}}
|
||||
called := false
|
||||
handler := withPermission(func(ctx *ext.Context, u *ext.Update) error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
if err := handler(&ext.Context{}, update); err != nil {
|
||||
t.Fatalf("withPermission returned error: %v", err)
|
||||
}
|
||||
if !called {
|
||||
t.Fatal("withPermission did not invoke the wrapped handler")
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func handleWatchCmd(ctx *ext.Context, update *ext.Update) error {
|
||||
filter := ""
|
||||
if len(args) > 2 {
|
||||
filterArg := strings.Join(args[2:], " ")
|
||||
filterType, _, _ := strings.Cut(filterArg, ":")
|
||||
filterType := strings.Split(filterArg, ":")[0]
|
||||
filterData := strings.Split(filterArg, ":")[1]
|
||||
if filterType == "" || filterData == "" {
|
||||
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgWatchErrorFilterFormatInvalid)), nil)
|
||||
|
||||
@@ -351,7 +351,7 @@ bot:
|
||||
info_filename_prefix: "Filename: "
|
||||
info_prompt_select_storage: "\nPlease select storage"
|
||||
progress:
|
||||
batch_status_header: "<b>📦 Processing</b>\n\nFiles: <code>{{.Total}}</code> | Total size: <code>{{.TotalSize}}</code>\nStatus: ✅ <code>{{.Completed}}</code> | 📥 <code>{{.Downloaded}}</code> | ⏳ <code>{{.Waiting}}</code>\nTotal speed: ⬇️ <code>{{.DownloadSpeed}}</code> | ⬆️ <code>{{.UploadSpeed}}</code>"
|
||||
batch_status_header: "<b>📦 Processing</b>\n\nFiles: <code>{{.Total}}</code>\nStatus: ✅ <code>{{.Completed}}</code> | 📥 <code>{{.Downloaded}}</code> | ⏳ <code>{{.Waiting}}</code>\nTotal speed: ⬇️ <code>{{.DownloadSpeed}}</code> | ⬆️ <code>{{.UploadSpeed}}</code>"
|
||||
batch_item_downloading: "<blockquote><b>⬇️ {{.Index}}/{{.Total}} Downloading</b>\n<code>{{.Name}}</code>\n{{.Bar}} <code>{{.Progress}}%</code>\nSpeed: <code>{{.Speed}}</code>\nSize: <code>{{.Current}}</code> / <code>{{.Size}}</code></blockquote>"
|
||||
batch_item_downloading_unknown: "<blockquote><b>⬇️ {{.Index}}/{{.Total}} Downloading</b>\n<code>{{.Name}}</code>\nSpeed: <code>{{.Speed}}</code>\nSize: <code>{{.Current}}</code> / unknown</blockquote>"
|
||||
batch_item_transferring: "<blockquote><b>↕️ {{.Index}}/{{.Total}} Transferring</b>\n<code>{{.Name}}</code>\n{{.Bar}} <code>{{.Progress}}%</code>\nSpeed: <code>{{.Speed}}</code>\nSize: <code>{{.Current}}</code> / <code>{{.Size}}</code></blockquote>"
|
||||
|
||||
@@ -352,7 +352,7 @@ bot:
|
||||
info_filename_prefix: "文件名: "
|
||||
info_prompt_select_storage: "\n请选择存储位置"
|
||||
progress:
|
||||
batch_status_header: "<b>📦 正在处理</b>\n\n文件:<code>{{.Total}}</code> | 总大小:<code>{{.TotalSize}}</code>\n状态:✅ <code>{{.Completed}}</code> | 📥 <code>{{.Downloaded}}</code> | ⏳ <code>{{.Waiting}}</code>\n总速度:⬇️ <code>{{.DownloadSpeed}}</code> | ⬆️ <code>{{.UploadSpeed}}</code>"
|
||||
batch_status_header: "<b>📦 正在处理</b>\n\n文件:<code>{{.Total}}</code>\n状态:✅ <code>{{.Completed}}</code> | 📥 <code>{{.Downloaded}}</code> | ⏳ <code>{{.Waiting}}</code>\n总速度:⬇️ <code>{{.DownloadSpeed}}</code> | ⬆️ <code>{{.UploadSpeed}}</code>"
|
||||
batch_item_downloading: "<blockquote><b>⬇️ {{.Index}}/{{.Total}} 下载中</b>\n<code>{{.Name}}</code>\n{{.Bar}} <code>{{.Progress}}%</code>\n速度:<code>{{.Speed}}</code>\n大小:<code>{{.Current}}</code> / <code>{{.Size}}</code></blockquote>"
|
||||
batch_item_downloading_unknown: "<blockquote><b>⬇️ {{.Index}}/{{.Total}} 下载中</b>\n<code>{{.Name}}</code>\n速度:<code>{{.Speed}}</code>\n大小:<code>{{.Current}}</code> / 未知</blockquote>"
|
||||
batch_item_transferring: "<blockquote><b>↕️ {{.Index}}/{{.Total}} 传输中</b>\n<code>{{.Name}}</code>\n{{.Bar}} <code>{{.Progress}}%</code>\n速度:<code>{{.Speed}}</code>\n大小:<code>{{.Current}}</code> / <code>{{.Size}}</code></blockquote>"
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package tdler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gotd/td/telegram/downloader"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/krau/SaveAny-Bot/common/utils/dlutil"
|
||||
"github.com/krau/SaveAny-Bot/config"
|
||||
"github.com/krau/SaveAny-Bot/pkg/consts/tglimit"
|
||||
@@ -13,23 +10,5 @@ import (
|
||||
|
||||
func NewDownloader(file tfile.TGFile) *downloader.Builder {
|
||||
return downloader.NewDownloader().WithPartSize(tglimit.MaxPartSize).
|
||||
Download(eofAwareClient{Client: file.Dler(), size: file.Size()}, file.Location()).
|
||||
WithThreads(dlutil.BestThreads(file.Size(), config.C().Threads))
|
||||
}
|
||||
|
||||
// eofAwareClient answers upload.getFile requests at or past the end of the
|
||||
// file with an empty chunk. gotd's downloader is size-unaware: for files
|
||||
// whose size is an exact multiple of the part size it issues one final
|
||||
// request at offset == size and expects an empty chunk, but Telegram rejects
|
||||
// it with 400 OFFSET_INVALID and the whole download fails.
|
||||
type eofAwareClient struct {
|
||||
downloader.Client
|
||||
size int64
|
||||
}
|
||||
|
||||
func (c eofAwareClient) UploadGetFile(ctx context.Context, req *tg.UploadGetFileRequest) (tg.UploadFileClass, error) {
|
||||
if req.Offset >= c.size {
|
||||
return &tg.UploadFile{}, nil
|
||||
}
|
||||
return c.Client.UploadGetFile(ctx, req)
|
||||
Download(file.Dler(), file.Location()).WithThreads(dlutil.BestThreads(file.Size(), config.C().Threads))
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
package tdler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/gotd/td/tgerr"
|
||||
|
||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||
)
|
||||
|
||||
// serverLikeClient mimics real Telegram upload.getFile behavior: it returns
|
||||
// up to limit bytes per chunk, and answers any offset at or past the end of
|
||||
// the file with 400 OFFSET_INVALID.
|
||||
type serverLikeClient struct {
|
||||
data []byte
|
||||
|
||||
mu sync.Mutex
|
||||
maxOffset int64
|
||||
}
|
||||
|
||||
func (c *serverLikeClient) UploadGetFile(_ context.Context, req *tg.UploadGetFileRequest) (tg.UploadFileClass, error) {
|
||||
c.mu.Lock()
|
||||
if req.Offset > c.maxOffset {
|
||||
c.maxOffset = req.Offset
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if req.Offset >= int64(len(c.data)) {
|
||||
return nil, tgerr.New(400, "OFFSET_INVALID")
|
||||
}
|
||||
end := min(len(c.data), int(req.Offset)+req.Limit)
|
||||
return &tg.UploadFile{Bytes: c.data[req.Offset:end]}, nil
|
||||
}
|
||||
|
||||
func (c *serverLikeClient) UploadGetFileHashes(context.Context, *tg.UploadGetFileHashesRequest) ([]tg.FileHash, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *serverLikeClient) UploadReuploadCDNFile(context.Context, *tg.UploadReuploadCDNFileRequest) ([]tg.FileHash, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *serverLikeClient) UploadGetCDNFileHashes(context.Context, *tg.UploadGetCDNFileHashesRequest) ([]tg.FileHash, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *serverLikeClient) UploadGetWebFile(context.Context, *tg.UploadGetWebFileRequest) (*tg.UploadWebFile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type memWriterAt struct {
|
||||
b []byte
|
||||
}
|
||||
|
||||
func (w *memWriterAt) WriteAt(p []byte, off int64) (int, error) {
|
||||
copy(w.b[off:], p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestDownloadServerLikeEOF(t *testing.T) {
|
||||
const partSize = 1024 * 1024
|
||||
tests := []struct {
|
||||
name string
|
||||
size int
|
||||
parallel bool
|
||||
}{
|
||||
{"stream exact multiple of part size", 2 * partSize, false},
|
||||
{"stream non-multiple", 2*partSize + 12345, false},
|
||||
{"stream smaller than part size", 1234, false},
|
||||
{"parallel exact multiple of part size", 2 * partSize, true},
|
||||
{"parallel non-multiple", 2*partSize + 12345, true},
|
||||
{"parallel smaller than part size", 1234, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data := make([]byte, tt.size)
|
||||
for i := range data {
|
||||
data[i] = byte(i % 251)
|
||||
}
|
||||
client := &serverLikeClient{data: data}
|
||||
file := tfile.NewTGFile(
|
||||
&tg.InputDocumentFileLocation{ID: 1, AccessHash: 2},
|
||||
client, int64(tt.size), "test.bin",
|
||||
)
|
||||
|
||||
dl := NewDownloader(file)
|
||||
var got []byte
|
||||
var err error
|
||||
if tt.parallel {
|
||||
buf := make([]byte, tt.size)
|
||||
_, err = dl.WithThreads(4).Parallel(context.Background(), &memWriterAt{b: buf})
|
||||
got = buf
|
||||
} else {
|
||||
var buf bytes.Buffer
|
||||
_, err = dl.Stream(context.Background(), &buf)
|
||||
got = buf.Bytes()
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("download failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, data) {
|
||||
t.Fatalf("downloaded %d bytes, want %d matching bytes", len(got), len(data))
|
||||
}
|
||||
if client.maxOffset >= int64(tt.size) {
|
||||
t.Fatalf("requested offset %d at or past EOF (size %d)", client.maxOffset, tt.size)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,6 @@ func buildBatchProgressMessage(info TaskInfo, skipped []string, activeLimit int)
|
||||
uploadSpeedText := formatSpeed(uploadSpeed)
|
||||
header := localizedProgressMarkup(i18nk.BotMsgProgressBatchStatusHeader, map[string]any{
|
||||
"Total": total,
|
||||
"TotalSize": dlutil.FormatSize(info.ActualTotalSize()),
|
||||
"Completed": completed,
|
||||
"Downloaded": downloaded,
|
||||
"Waiting": waiting,
|
||||
|
||||
@@ -95,30 +95,6 @@ func TestBatchProgressShowsTransferSpeedAndSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchProgressHeaderShowsTotalSize(t *testing.T) {
|
||||
useProgressRegressionLocale(t)
|
||||
task := newProgressRegressionTask(nil,
|
||||
progressRegressionFile{"first", 1024},
|
||||
progressRegressionFile{"second", 1024},
|
||||
)
|
||||
message := buildBatchProgressMessage(task, nil, 2)
|
||||
if message.Err != nil {
|
||||
t.Fatalf("buildBatchProgressMessage() failed: %v", message.Err)
|
||||
}
|
||||
assertProgressRegressionContains(t, message.Text,
|
||||
"文件:2 | 总大小:2.00 KB",
|
||||
)
|
||||
|
||||
i18n.Init("en")
|
||||
english := buildBatchProgressMessage(task, nil, 2)
|
||||
if english.Err != nil {
|
||||
t.Fatalf("English batch template failed: %v", english.Err)
|
||||
}
|
||||
assertProgressRegressionContains(t, english.Text,
|
||||
"Files: 2 | Total size: 2.00 KB",
|
||||
)
|
||||
}
|
||||
|
||||
func TestBatchProgressLimitsRowsWithoutHidingActiveUpload(t *testing.T) {
|
||||
useProgressRegressionLocale(t)
|
||||
task := newProgressRegressionTask(nil,
|
||||
|
||||
Reference in New Issue
Block a user