// 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) }