增加精确修复IM账号工具
This commit is contained in:
parent
cfba26a2f3
commit
5a674a8c98
119
services/notice-service/cmd/import-im-accounts/main.go
Normal file
119
services/notice-service/cmd/import-im-accounts/main.go
Normal file
@ -0,0 +1,119 @@
|
||||
// Command import-im-accounts performs an explicit, bounded Tencent IM account
|
||||
// repair before replaying private notices that failed with invalid identifier.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp/pkg/tencentim"
|
||||
"hyapp/services/notice-service/internal/config"
|
||||
)
|
||||
|
||||
const executeConfirmation = "IMPORT_EXACT_TENCENT_IM_ACCOUNTS"
|
||||
|
||||
type userIDFlags []int64
|
||||
|
||||
func (values *userIDFlags) String() string {
|
||||
parts := make([]string, 0, len(*values))
|
||||
for _, value := range *values {
|
||||
parts = append(parts, strconv.FormatInt(value, 10))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func (values *userIDFlags) Set(raw string) error {
|
||||
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
||||
if err != nil || value <= 0 {
|
||||
return fmt.Errorf("user-id must be a positive int64")
|
||||
}
|
||||
*values = append(*values, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "services/notice-service/configs/config.yaml", "notice-service config containing Tencent IM credentials")
|
||||
execute := flag.Bool("execute", false, "perform the exact account imports; default is dry-run")
|
||||
confirmation := flag.String("confirm", "", "required execution confirmation")
|
||||
timeout := flag.Duration("timeout", 30*time.Second, "total import timeout")
|
||||
var requested userIDFlags
|
||||
flag.Var(&requested, "user-id", "exact internal user_id to import; repeat for multiple users")
|
||||
flag.Parse()
|
||||
|
||||
userIDs, err := normalizeUserIDs(requested)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if *timeout <= 0 || *timeout > 5*time.Minute {
|
||||
fatal(fmt.Errorf("timeout must be within (0,5m]"))
|
||||
}
|
||||
if *execute && strings.TrimSpace(*confirmation) != executeConfirmation {
|
||||
fatal(fmt.Errorf("--execute requires --confirm=%s", executeConfirmation))
|
||||
}
|
||||
fingerprint := userIDsSHA256(userIDs)
|
||||
if !*execute {
|
||||
fmt.Printf("mode=dry-run users=%d user_ids_sha256=%s\n", len(userIDs), fingerprint)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Load(strings.TrimSpace(*configPath))
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if !cfg.TencentIM.Enabled {
|
||||
fatal(fmt.Errorf("tencent_im.enabled is required"))
|
||||
}
|
||||
client, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
defer cancel()
|
||||
for _, userID := range userIDs {
|
||||
// Account import is idempotent in the shared REST client: Tencent's
|
||||
// already-exists response is success, while transport and other API errors
|
||||
// stop the bounded batch before the notice replay is attempted.
|
||||
if err := client.ImportAccount(ctx, tencentim.AccountProfile{UserID: userID}); err != nil {
|
||||
fatal(fmt.Errorf("import user index %d: %w", sort.Search(len(userIDs), func(index int) bool { return userIDs[index] >= userID })+1, err))
|
||||
}
|
||||
}
|
||||
fmt.Printf("mode=execute imported=%d user_ids_sha256=%s\n", len(userIDs), fingerprint)
|
||||
}
|
||||
|
||||
func normalizeUserIDs(values []int64) ([]int64, error) {
|
||||
if len(values) == 0 || len(values) > 100 {
|
||||
return nil, fmt.Errorf("user-id count must be between 1 and 100")
|
||||
}
|
||||
normalized := append([]int64(nil), values...)
|
||||
sort.Slice(normalized, func(i, j int) bool { return normalized[i] < normalized[j] })
|
||||
result := normalized[:0]
|
||||
for _, value := range normalized {
|
||||
if value <= 0 {
|
||||
return nil, fmt.Errorf("user-id must be a positive int64")
|
||||
}
|
||||
if len(result) == 0 || result[len(result)-1] != value {
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func userIDsSHA256(values []int64) string {
|
||||
parts := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
parts = append(parts, strconv.FormatInt(value, 10))
|
||||
}
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(parts, "\n"))))
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
24
services/notice-service/cmd/import-im-accounts/main_test.go
Normal file
24
services/notice-service/cmd/import-im-accounts/main_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeUserIDsSortsAndDeduplicatesExactSet(t *testing.T) {
|
||||
got, err := normalizeUserIDs([]int64{42, 7, 42})
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeUserIDs failed: %v", err)
|
||||
}
|
||||
if len(got) != 2 || got[0] != 7 || got[1] != 42 {
|
||||
t.Fatalf("normalized IDs mismatch: %+v", got)
|
||||
}
|
||||
if fingerprint := userIDsSHA256(got); fingerprint != userIDsSHA256([]int64{7, 42}) {
|
||||
t.Fatalf("fingerprint must be stable, got %s", fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeUserIDsRejectsUnsafeSets(t *testing.T) {
|
||||
for _, values := range [][]int64{nil, {0}, {-1}} {
|
||||
if _, err := normalizeUserIDs(values); err == nil {
|
||||
t.Fatalf("unsafe user IDs must fail: %+v", values)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user