// Package tencentim contains the server-side Tencent Cloud Chat integration boundary. package tencentim import ( "bytes" "compress/zlib" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "strconv" "strings" "time" ) // UserSigConfig describes the stable inputs required by Tencent Cloud Chat UserSig. type UserSigConfig struct { // SDKAppID is the application ID assigned by Tencent Cloud Chat. SDKAppID int64 // SecretKey is the application key; it must stay on backend services only. SecretKey string // TTL controls how long the generated UserSig remains valid. TTL time.Duration } // UserSigResult is returned by gateway-service to authenticated clients. type UserSigResult struct { // SDKAppID lets clients initialize the Tencent Cloud Chat SDK. SDKAppID int64 `json:"sdk_app_id"` // UserID is the Tencent Chat identifier; this project uses decimal user_id. UserID string `json:"user_id"` // UserSig is the signed credential passed to Tencent Cloud Chat SDK login. UserSig string `json:"user_sig"` // ExpireAtMS lets clients refresh the credential before Tencent rejects login. ExpireAtMS int64 `json:"expire_at_ms"` } // GenerateUserSig signs a Tencent Cloud Chat UserSig with the HMAC-SHA256 v2 algorithm. func GenerateUserSig(cfg UserSigConfig, userID string, now time.Time) (UserSigResult, error) { userID = strings.TrimSpace(userID) if cfg.SDKAppID <= 0 || cfg.SecretKey == "" || cfg.TTL <= 0 || userID == "" { // UserSig protects paid Tencent resources; incomplete config must fail closed. return UserSigResult{}, fmt.Errorf("tencent im usersig config is incomplete") } current := now.Unix() expire := int64(cfg.TTL.Seconds()) rawSig := signUserSigPayload(cfg.SecretKey, userID, cfg.SDKAppID, current, expire) payload := map[string]any{ "TLS.ver": "2.0", "TLS.identifier": userID, "TLS.sdkappid": cfg.SDKAppID, "TLS.expire": expire, "TLS.time": current, "TLS.sig": rawSig, } payloadBytes, err := json.Marshal(payload) if err != nil { return UserSigResult{}, err } compressed, err := zlibCompress(payloadBytes) if err != nil { return UserSigResult{}, err } return UserSigResult{ SDKAppID: cfg.SDKAppID, UserID: userID, UserSig: encodeTencentBase64(compressed), ExpireAtMS: now.Add(cfg.TTL).UnixMilli(), }, nil } func signUserSigPayload(secret string, userID string, sdkAppID int64, current int64, expire int64) string { // Tencent v2 signing string is line-based and key names are case-sensitive. content := "TLS.identifier:" + userID + "\n" + "TLS.sdkappid:" + strconv.FormatInt(sdkAppID, 10) + "\n" + "TLS.time:" + strconv.FormatInt(current, 10) + "\n" + "TLS.expire:" + strconv.FormatInt(expire, 10) + "\n" mac := hmac.New(sha256.New, []byte(secret)) _, _ = mac.Write([]byte(content)) return base64.StdEncoding.EncodeToString(mac.Sum(nil)) } func zlibCompress(payload []byte) ([]byte, error) { var buffer bytes.Buffer writer := zlib.NewWriter(&buffer) if _, err := writer.Write(payload); err != nil { _ = writer.Close() return nil, err } if err := writer.Close(); err != nil { return nil, err } return buffer.Bytes(), nil } func encodeTencentBase64(payload []byte) string { // Tencent UserSig uses standard base64 with URL-hostile characters remapped. encoded := base64.StdEncoding.EncodeToString(payload) encoded = strings.ReplaceAll(encoded, "+", "*") encoded = strings.ReplaceAll(encoded, "/", "-") encoded = strings.ReplaceAll(encoded, "=", "_") return encoded }