Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 130 additions & 2 deletions client/bot/handlers/add_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import (
"github.com/charmbracelet/log"
"github.com/gotd/td/tg"
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/ruleutil"
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
"github.com/krau/SaveAny-Bot/common/i18n"
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
"github.com/krau/SaveAny-Bot/database"
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
"github.com/krau/SaveAny-Bot/pkg/tfile"
"github.com/krau/SaveAny-Bot/storage"
"gorm.io/gorm"
)
Expand Down Expand Up @@ -73,14 +75,41 @@ func handleAddCallback(ctx *ext.Context, update *ext.Update) error {
return dispatcher.EndGroups
}
dirPath = dir.Path
} else if data.SelectedDirPath != "" {
dirPath = data.SelectedDirPath
}

switch data.TaskType {
case tasktype.TaskTypeTgfiles:
strategy, err := getEffectiveConflictStrategy(ctx, userID, data.ConflictStrategy)
if err != nil {
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, err.Error()))
return dispatcher.EndGroups
}
conflicts, err := findTGFileConflicts(ctx, userID, selectedStorage, dirPath, data.Files)
if err != nil {
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, err.Error()))
return dispatcher.EndGroups
}
if len(conflicts) > 0 && strategy == tcbdata.ConflictStrategyAsk {
markup, err := msgelem.BuildConflictStrategyMarkup(data)
if err != nil {
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, i18n.T(i18nk.BotMsgCommonErrorBuildStorageSelectKeyboardFailed, map[string]any{
"Error": err.Error(),
})))
return dispatcher.EndGroups
}
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: update.CallbackQuery.GetMsgID(),
Message: i18n.T(i18nk.BotMsgCommonPromptSelectConflictStrategy, map[string]any{"Files": formatConflictFiles(conflicts)}),
ReplyMarkup: markup,
})
return dispatcher.EndGroups
Comment on lines +89 to +107
}
if data.AsBatch {
return shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files, msgID)
return shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files, msgID, strategy)
}
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files[0], msgID)
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files[0], msgID, strategy)
case tasktype.TaskTypeTphpics:
return shortcut.CreateAndAddtelegraphWithEdit(ctx, userID, data.TphPageNode, data.TphDirPath, data.TphPics, selectedStorage, msgID)
case tasktype.TaskTypeParseditem:
Expand Down Expand Up @@ -108,3 +137,102 @@ func handleAddCallback(ctx *ext.Context, update *ext.Update) error {
}
return dispatcher.EndGroups
}

func getEffectiveConflictStrategy(ctx *ext.Context, userID int64, selected string) (string, error) {
if tcbdata.IsConflictStrategy(selected) {
return selected, nil
}
user, err := database.GetUserByChatID(ctx, userID)
if err != nil {
return "", errors.New(i18n.T(i18nk.BotMsgCommonErrorGetUserWithErrFailed, map[string]any{"Error": err.Error()}))
}
return effectiveUserConflictStrategy(user), nil
}

type tgFileConflict struct {
Name string
StorageName string
Path string
}

func findTGFileConflicts(ctx *ext.Context, userID int64, stor storage.Storage, dirPath string, files []tfile.TGFileMessage) ([]tgFileConflict, error) {
user, err := database.GetUserByChatID(ctx, userID)
if err != nil {
return nil, errors.New(i18n.T(i18nk.BotMsgCommonErrorGetUserWithErrFailed, map[string]any{"Error": err.Error()}))
}
useRule := user.ApplyRule && user.Rules != nil
conflicts := make([]tgFileConflict, 0)

resolve := func(file tfile.TGFileMessage) (storage.Storage, string, error) {
fileStor := stor
fileDirPath := dirPath
if useRule {
matched, matchedStorageName, matchedDirPath := ruleutil.ApplyRule(ctx, user.Rules, ruleutil.NewInput(file))
if matched {
if matchedDirPath != "" {
fileDirPath = matchedDirPath.String()
}
if matchedStorageName.Usable() {
var err error
fileStor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, matchedStorageName.String())
if err != nil {
return nil, "", errors.New(i18n.T(i18nk.BotMsgCommonErrorGetStorageFailed, map[string]any{"Error": err.Error()}))
}
}
}
}
return fileStor, fileDirPath, nil
}

albumFiles := make(map[int64][]tfile.TGFileMessage)
for _, file := range files {
fileStor, fileDirPath, err := resolve(file)
if err != nil {
return nil, err
}
if ruleutil.MatchedDirPath(fileDirPath).NeedNewForAlbum() {
groupID, isGroup := file.Message().GetGroupedID()
if isGroup && groupID != 0 {
albumFiles[groupID] = append(albumFiles[groupID], file)
}
continue
}
storagePath := path.Join(fileDirPath, file.Name())
if fileStor.Exists(ctx, storagePath) {
conflicts = append(conflicts, tgFileConflict{Name: file.Name(), StorageName: fileStor.Name(), Path: storagePath})
}
}

for _, afiles := range albumFiles {
if len(afiles) <= 1 {
continue
}
albumDir := strings.TrimSuffix(path.Base(afiles[0].Name()), path.Ext(afiles[0].Name()))
albumStor, _, err := resolve(afiles[0])
if err != nil {
return nil, err
}
for _, file := range afiles {
storagePath := path.Join(dirPath, albumDir, file.Name())
if albumStor.Exists(ctx, storagePath) {
conflicts = append(conflicts, tgFileConflict{Name: file.Name(), StorageName: albumStor.Name(), Path: storagePath})
}
}
}
return conflicts, nil
}

func formatConflictFiles(conflicts []tgFileConflict) string {
const maxConflictLines = 10
var b strings.Builder
for i, conflict := range conflicts {
if i >= maxConflictLines {
fmt.Fprint(&b, i18n.T(i18nk.BotMsgCommonPromptConflictMoreFiles, map[string]any{
"Count": len(conflicts) - maxConflictLines,
}))
break
}
fmt.Fprintf(&b, "- [%s]:%s\n", conflict.StorageName, conflict.Path)
}
return strings.TrimSpace(b.String())
}
77 changes: 77 additions & 0 deletions client/bot/handlers/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ func handleConfigCmd(ctx *ext.Context, update *ext.Update) error {
Text: i18n.T(i18nk.BotMsgConfigButtonFilenameStrategy),
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeConfig, "fnamest"),
},
&tg.KeyboardButtonCallback{
Text: i18n.T(i18nk.BotMsgConfigButtonConflictStrategy),
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeConfig, "conflictst"),
},
},
},
},
Expand All @@ -51,6 +55,8 @@ func handleConfigCallback(ctx *ext.Context, update *ext.Update) error {
switch args[1] {
case "fnamest":
return handleConfigFnameSTCallback(ctx, update)
case "conflictst":
return handleConfigConflictSTCallback(ctx, update)
default:
return invaildDataAnswer()
}
Expand Down Expand Up @@ -110,6 +116,77 @@ func handleConfigFnameSTCallback(ctx *ext.Context, update *ext.Update) error {
return dispatcher.EndGroups
}

func handleConfigConflictSTCallback(ctx *ext.Context, update *ext.Update) error {
userID := update.CallbackQuery.GetUserID()
user, err := database.GetUserByChatID(ctx, userID)
if err != nil {
return err
}
args := strings.Fields(string(update.CallbackQuery.Data))
if len(args) == 3 {
selected := args[2]
if !tcbdata.IsConflictStrategy(selected) {
return fmt.Errorf("invalid conflict strategy: %s", selected)
}
user.ConflictStrategy = selected
if err := database.UpdateUser(ctx, user); err != nil {
return err
}
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: update.CallbackQuery.GetMsgID(),
Message: i18n.T(i18nk.BotMsgConfigInfoConflictStrategySet, map[string]any{
"Strategy": conflictStrategyDisplay(selected),
}),
})
return dispatcher.EndGroups
}

opts := tcbdata.ConflictStrategyValues()
rows := make([]tg.KeyboardButtonRow, 0, len(opts))
for _, opt := range opts {
rows = append(rows, tg.KeyboardButtonRow{
Buttons: []tg.KeyboardButtonClass{
&tg.KeyboardButtonCallback{
Text: conflictStrategyDisplay(opt),
Data: fmt.Appendf(nil, "%s %s %s", tcbdata.TypeConfig, "conflictst", opt),
},
},
})
}
markup := &tg.ReplyInlineMarkup{Rows: rows}
currentSt := effectiveUserConflictStrategy(user)
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: update.CallbackQuery.GetMsgID(),
Message: i18n.T(i18nk.BotMsgConfigPromptSelectConflictStrategy, map[string]any{
"Strategy": conflictStrategyDisplay(currentSt),
}),
ReplyMarkup: markup,
})
return dispatcher.EndGroups
}

func effectiveUserConflictStrategy(user *database.User) string {
if user != nil && tcbdata.IsConflictStrategy(user.ConflictStrategy) {
return user.ConflictStrategy
}
return tcbdata.ConflictStrategyRename
}

func conflictStrategyDisplay(strategy string) string {
switch strategy {
case tcbdata.ConflictStrategyRename:
return i18n.T(i18nk.BotMsgConfigConflictStrategyRename, nil)
case tcbdata.ConflictStrategyAsk:
return i18n.T(i18nk.BotMsgConfigConflictStrategyAsk, nil)
case tcbdata.ConflictStrategyOverwrite:
return i18n.T(i18nk.BotMsgConfigConflictStrategyOverwrite, nil)
case tcbdata.ConflictStrategySkip:
return i18n.T(i18nk.BotMsgConfigConflictStrategySkip, nil)
default:
return strategy
}
}

func handleConfigFnameTmpl(ctx *ext.Context, update *ext.Update) error {
userID := update.GetUserChat().GetID()
user, err := database.GetUserByChatID(ctx, userID)
Expand Down
34 changes: 34 additions & 0 deletions client/bot/handlers/utils/msgelem/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ func BuildAddSelectStorageKeyboard(stors []storage.Storage, adddata tcbdata.Add)
data := tcbdata.Add{
TaskType: taskType,
SelectedStorName: storage.Name(),
SelectedDirPath: adddata.SelectedDirPath,
ConflictStrategy: adddata.ConflictStrategy,

Files: adddata.Files,
AsBatch: len(adddata.Files) > 1,
Expand Down Expand Up @@ -109,6 +111,38 @@ func BuildAddOneSelectStorageMessage(ctx context.Context, stors []storage.Storag
}, nil
}

func BuildConflictStrategyMarkup(adddata tcbdata.Add) (*tg.ReplyInlineMarkup, error) {
type option struct {
text string
strategy string
}
options := []option{
{text: i18n.T(i18nk.BotMsgCommonButtonConflictRename, nil), strategy: tcbdata.ConflictStrategyRename},
{text: i18n.T(i18nk.BotMsgCommonButtonConflictOverwrite, nil), strategy: tcbdata.ConflictStrategyOverwrite},
{text: i18n.T(i18nk.BotMsgCommonButtonConflictSkip, nil), strategy: tcbdata.ConflictStrategySkip},
}
buttons := make([]tg.KeyboardButtonClass, 0, len(options))
for _, opt := range options {
data := adddata
data.ConflictStrategy = opt.strategy
dataid := xid.New().String()
if err := cache.Set(dataid, data); err != nil {
return nil, err
}
buttons = append(buttons, &tg.KeyboardButtonCallback{
Text: opt.text,
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeAdd, dataid),
})
}
rows := make([]tg.KeyboardButtonRow, 0, len(buttons))
for _, button := range buttons {
rows = append(rows, tg.KeyboardButtonRow{
Buttons: []tg.KeyboardButtonClass{button},
})
}
return &tg.ReplyInlineMarkup{Rows: rows}, nil
}

// Builds the inline keyboard for setting default storage
func BuildSetDefaultStorageMarkup(
ctx context.Context,
Expand Down
Loading