-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Fix mail rule reorder with partial IDs #2175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2561652
98c5edb
25fe8bb
a3b1d7b
92eea87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package service | ||
|
|
||
| import ( | ||
| "context" | ||
| "strings" | ||
|
|
||
| "github.com/larksuite/cli/errs" | ||
| "github.com/larksuite/cli/internal/client" | ||
| "github.com/larksuite/cli/internal/core" | ||
| ) | ||
|
|
||
| const mailRulesReorderSchemaPath = "mail.user_mailbox.rules.reorder" | ||
|
|
||
| func maybeCompleteMailRulesReorderIDs(ctx context.Context, ac *client.APIClient, opts *ServiceMethodOptions, request *client.RawApiRequest, checkErr func(interface{}, core.Identity) error) error { | ||
| if opts.SchemaPath != mailRulesReorderSchemaPath { | ||
| return nil | ||
| } | ||
|
|
||
| body, ok := toStringAnyMap(request.Data) | ||
| if !ok { | ||
| return errs.NewValidationError(errs.SubtypeInvalidArgument, "mail rules reorder requires a JSON object body").WithParam("--data") | ||
| } | ||
|
|
||
| requestedIDs, err := mailRuleIDsFromBody(body) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if err := validateRequestedMailRuleIDs(requestedIDs); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| existingIDs, err := listAllMailRuleIDs(ctx, ac, *request, checkErr) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| completedIDs, err := completeMailRuleIDs(requestedIDs, existingIDs) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| body["rule_ids"] = completedIDs | ||
| request.Data = body | ||
| return nil | ||
| } | ||
|
|
||
| func toStringAnyMap(v any) (map[string]any, bool) { | ||
| if m, ok := v.(map[string]any); ok { | ||
| return m, true | ||
| } | ||
| return nil, false | ||
| } | ||
|
|
||
| func mailRuleIDsFromBody(body map[string]any) ([]string, error) { | ||
| raw, ok := body["rule_ids"] | ||
| if !ok { | ||
| return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "rule_ids is required").WithParam("rule_ids") | ||
| } | ||
| switch ids := raw.(type) { | ||
| case []string: | ||
| return append([]string(nil), ids...), nil | ||
| case []any: | ||
| out := make([]string, 0, len(ids)) | ||
| for i, id := range ids { | ||
| s, ok := id.(string) | ||
| if !ok { | ||
| return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "rule_ids[%d] must be a string", i).WithParam("rule_ids") | ||
| } | ||
| out = append(out, s) | ||
| } | ||
| return out, nil | ||
| default: | ||
| return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "rule_ids must be an array of strings").WithParam("rule_ids") | ||
| } | ||
| } | ||
|
|
||
| func validateRequestedMailRuleIDs(requestedIDs []string) error { | ||
| if len(requestedIDs) == 0 { | ||
| return errs.NewValidationError(errs.SubtypeInvalidArgument, "at least one rule_id is required for reorder").WithParam("rule_ids") | ||
| } | ||
| seen := map[string]struct{}{} | ||
| for _, id := range requestedIDs { | ||
| if id == "" { | ||
| return errs.NewValidationError(errs.SubtypeInvalidArgument, "rule_ids cannot contain an empty string").WithParam("rule_ids") | ||
| } | ||
| if _, ok := seen[id]; ok { | ||
| return errs.NewValidationError(errs.SubtypeInvalidArgument, "duplicate rule_id: %s", id).WithParam("rule_ids") | ||
| } | ||
| seen[id] = struct{}{} | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func completeMailRuleIDs(requestedIDs, existingIDs []string) ([]string, error) { | ||
| existingSet := make(map[string]struct{}, len(existingIDs)) | ||
| for _, id := range existingIDs { | ||
| if id == "" { | ||
| continue | ||
| } | ||
| existingSet[id] = struct{}{} | ||
| } | ||
| for _, id := range requestedIDs { | ||
| if _, ok := existingSet[id]; !ok { | ||
| return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "unknown rule_id: %s", id).WithParam("rule_ids") | ||
| } | ||
| } | ||
|
|
||
| requestedSet := make(map[string]struct{}, len(requestedIDs)) | ||
| completed := make([]string, 0, len(existingIDs)) | ||
| for _, id := range requestedIDs { | ||
| requestedSet[id] = struct{}{} | ||
| completed = append(completed, id) | ||
| } | ||
| for _, id := range existingIDs { | ||
| if _, ok := requestedSet[id]; !ok { | ||
| completed = append(completed, id) | ||
| } | ||
| } | ||
| return completed, nil | ||
| } | ||
|
|
||
| func listAllMailRuleIDs(ctx context.Context, ac *client.APIClient, reorderRequest client.RawApiRequest, checkErr func(interface{}, core.Identity) error) ([]string, error) { | ||
| listRequest := client.RawApiRequest{ | ||
| Method: "GET", | ||
| URL: mailRulesListURL(reorderRequest.URL), | ||
| Params: copyRequestParamsWithoutPageToken(reorderRequest.Params), | ||
| As: reorderRequest.As, | ||
| } | ||
| result, err := ac.PaginateAll(ctx, listRequest, client.PaginationOptions{ | ||
| PageLimit: 0, | ||
| PageDelay: -1, | ||
| Identity: reorderRequest.As, | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if apiErr := checkErr(result, reorderRequest.As); apiErr != nil { | ||
| return nil, apiErr | ||
| } | ||
| return extractMailRuleIDs(result) | ||
| } | ||
|
|
||
| func mailRulesListURL(reorderURL string) string { | ||
| return strings.TrimSuffix(reorderURL, "/reorder") | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 AI Review | [P3 可维护性] 对远端 meta 形状的三处硬编码假设没有任何断言或告警保护 补齐能否生效同时依赖三个硬编码假设:schema path 常量 具体失败场景:远端 meta 把 schema path 改名(例如 修复建议: 在 如有疑问或认为判断不准确,欢迎直接回复讨论。 |
||
| } | ||
|
|
||
| func copyRequestParamsWithoutPageToken(in map[string]any) map[string]any { | ||
| out := make(map[string]any, len(in)) | ||
| for k, v := range in { | ||
| if k == "page_token" { | ||
| continue | ||
| } | ||
| out[k] = v | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| func extractMailRuleIDs(result any) ([]string, error) { | ||
| data, ok := nestedMap(result, "data") | ||
| if !ok { | ||
| return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "mail rules list response missing data") | ||
| } | ||
| rules, ok := data["items"].([]any) | ||
| if !ok { | ||
| rules, ok = data["rules"].([]any) | ||
| } | ||
| if !ok { | ||
| return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "mail rules list response missing items") | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 AI Review | [P2 正确性] 规则列表为空时报的是 internal error,而不是可操作的校验错误
具体失败场景:新邮箱一条收信规则都没有,用户执行 修复建议: 如有疑问或认为判断不准确,欢迎直接回复讨论。 |
||
| } | ||
|
|
||
| ids := make([]string, 0, len(rules)) | ||
| for i, item := range rules { | ||
| rule, ok := item.(map[string]any) | ||
| if !ok { | ||
| return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "mail rules list item %d is not an object", i) | ||
| } | ||
| id, ok := rule["rule_id"].(string) | ||
| if !ok || id == "" { | ||
| id, ok = rule["id"].(string) | ||
| } | ||
| if !ok || id == "" { | ||
| return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "mail rules list item %d missing rule_id", i) | ||
| } | ||
| ids = append(ids, id) | ||
| } | ||
| return ids, nil | ||
| } | ||
|
|
||
| func nestedMap(result any, key string) (map[string]any, bool) { | ||
| m, ok := toStringAnyMap(result) | ||
| if !ok { | ||
| return nil, false | ||
| } | ||
| return toStringAnyMap(m[key]) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 AI Review | [P1 稳定性] 分页中途失败会被静默吞掉,导致用不完整的规则列表去 reorder
PaginateAll内部的paginateLoop(internal/client/client.go:376-397)在第 2 页及以后失败时并不返回 error:网络错误走break,业务错误(code != 0)也是 append 后break,两种情况最终都返回allResults, nil。而mergePagedResults的顶层字段(含code)是从第一页复制的,所以第 139 行的checkErr(result, ...)只能看到第一页的code: 0,中途失败完全感知不到。listAllMailRuleIDs也没有检查合并结果里的data.has_more。具体失败场景:某邮箱规则有 2 页,第 2 页请求超时。此时
PaginateAll只返回第一页原始响应(len(results)==1直接返回results[0],其has_more仍为 true),existingIDs只含第一页 ID。接着分两种走向——(a) 用户要移动的规则正好在第 2 页:completeMailRuleIDs报unknown rule_id: xxx,误导用户以为该规则不存在;(b) 用户要移动的规则在第 1 页:补齐出的rule_ids缺少第 2 页规则,reorder 仍会触发后端“缺少规则 ID”的报错,本 PR 想解决的问题在分页场景下并未解决。修复建议:
PaginateAll返回后先校验完整性再继续——取data["has_more"],为true时直接返回明确错误(例如failed to fetch the complete rule list, please retry)而不是继续调用 reorder;同时补一条“第 2 页返回code != 0/ 网络失败”的 httpmock 单测,断言不发出 reorder 请求。如有疑问或认为判断不准确,欢迎直接回复讨论。