120 lines
3.7 KiB
Go
120 lines
3.7 KiB
Go
// 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)
|
|
}
|