// Package tencentrtc contains Tencent RTC token and server-side room APIs. package tencentrtc import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" ) const ( // DefaultRESTEndpoint is the Tencent Cloud API endpoint for TRTC room management. DefaultRESTEndpoint = "trtc.tencentcloudapi.com" // DefaultRESTRegion is accepted by TRTC room management APIs and keeps local config explicit. DefaultRESTRegion = "ap-guangzhou" trtcServiceName = "trtc" trtcAPIVersion = "2019-07-22" removeUserByStrRoomAction = "RemoveUserByStrRoomId" tencentCloudTC3Algorithm = "TC3-HMAC-SHA256" tencentCloudTC3RequestType = "tc3_request" ) // RESTConfig describes the Tencent Cloud API credentials used for RTC room management. type RESTConfig struct { Enabled bool // SDKAppID is the TRTC application ID that owns the room. SDKAppID int64 // SecretID/SecretKey are Tencent Cloud API credentials with permission on the TRTC SDKAppID. SecretID string SecretKey string // Region is required by Tencent Cloud API even though room IDs remain application-global. Region string // Endpoint defaults to trtc.tencentcloudapi.com. Endpoint string // HTTPClient lets tests inject a fake transport and lets services bound public network latency. HTTPClient *http.Client // Now is injectable so signature tests do not depend on wall clock time. Now func() time.Time } // RESTClient wraps Tencent Cloud API 3.0 calls needed by room-service. type RESTClient struct { cfg RESTConfig httpClient *http.Client now func() time.Time } // NewRESTClient validates the API credential boundary and returns a reusable client. func NewRESTClient(cfg RESTConfig) (*RESTClient, error) { cfg.SecretID = strings.TrimSpace(cfg.SecretID) cfg.SecretKey = strings.TrimSpace(cfg.SecretKey) cfg.Region = strings.TrimSpace(cfg.Region) cfg.Endpoint = strings.TrimSpace(cfg.Endpoint) if cfg.SDKAppID <= 0 || cfg.SecretID == "" || cfg.SecretKey == "" { return nil, fmt.Errorf("tencent rtc rest config is incomplete") } if cfg.Region == "" { cfg.Region = DefaultRESTRegion } if cfg.Endpoint == "" { cfg.Endpoint = DefaultRESTEndpoint } if cfg.HTTPClient == nil { cfg.HTTPClient = &http.Client{Timeout: 5 * time.Second} } if cfg.Now == nil { cfg.Now = func() time.Time { return time.Now().UTC() } } return &RESTClient{cfg: cfg, httpClient: cfg.HTTPClient, now: cfg.Now}, nil } // RemoveUserByStrRoomID removes a user from a string-ID TRTC room. // It only affects the live RTC room; room-service must update Room Cell state before calling this. func (c *RESTClient) RemoveUserByStrRoomID(ctx context.Context, roomID string, userID int64) error { roomID = strings.TrimSpace(roomID) if roomID == "" || userID <= 0 { return fmt.Errorf("rtc remove user is incomplete") } payload := removeUserByStrRoomRequest{ SDKAppID: c.cfg.SDKAppID, RoomID: roomID, UserIDs: []string{strconv.FormatInt(userID, 10)}, } return c.post(ctx, removeUserByStrRoomAction, payload) } func (c *RESTClient) post(ctx context.Context, action string, payload any) error { body, err := json.Marshal(payload) if err != nil { return err } request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://"+strings.TrimSuffix(c.cfg.Endpoint, "/"), bytes.NewReader(body)) if err != nil { return err } request.Header.Set("Content-Type", "application/json; charset=utf-8") request.Header.Set("Host", c.cfg.Endpoint) request.Header.Set("X-TC-Action", action) request.Header.Set("X-TC-Version", trtcAPIVersion) request.Header.Set("X-TC-Region", c.cfg.Region) c.sign(request, body, action) response, err := c.httpClient.Do(request) if err != nil { return err } defer response.Body.Close() responseBody, err := io.ReadAll(response.Body) if err != nil { return err } if response.StatusCode < 200 || response.StatusCode >= 300 { return fmt.Errorf("tencent rtc http status %d: %s", response.StatusCode, string(responseBody)) } var cloudResp cloudAPIResponse if err := json.Unmarshal(responseBody, &cloudResp); err != nil { return err } if cloudResp.Response.Error != nil { return fmt.Errorf("tencent rtc rest failed: code=%s message=%s", cloudResp.Response.Error.Code, cloudResp.Response.Error.Message) } return nil } func (c *RESTClient) sign(request *http.Request, body []byte, action string) { now := c.now().UTC() timestamp := now.Unix() date := now.Format("2006-01-02") request.Header.Set("X-TC-Timestamp", strconv.FormatInt(timestamp, 10)) hashedPayload := sha256Hex(body) canonicalHeaders := "content-type:" + strings.ToLower(request.Header.Get("Content-Type")) + "\n" + "host:" + c.cfg.Endpoint + "\n" signedHeaders := "content-type;host" canonicalRequest := strings.Join([]string{ http.MethodPost, "/", "", canonicalHeaders, signedHeaders, hashedPayload, }, "\n") credentialScope := date + "/" + trtcServiceName + "/" + tencentCloudTC3RequestType stringToSign := strings.Join([]string{ tencentCloudTC3Algorithm, strconv.FormatInt(timestamp, 10), credentialScope, sha256Hex([]byte(canonicalRequest)), }, "\n") signature := hex.EncodeToString(hmacSHA256(tc3SigningKey(c.cfg.SecretKey, date), stringToSign)) authorization := fmt.Sprintf( "%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", tencentCloudTC3Algorithm, c.cfg.SecretID, credentialScope, signedHeaders, signature, ) request.Header.Set("Authorization", authorization) _ = action } func tc3SigningKey(secretKey string, date string) []byte { secretDate := hmacSHA256([]byte("TC3"+secretKey), date) secretService := hmacSHA256(secretDate, trtcServiceName) return hmacSHA256(secretService, tencentCloudTC3RequestType) } func hmacSHA256(key []byte, value string) []byte { mac := hmac.New(sha256.New, key) _, _ = mac.Write([]byte(value)) return mac.Sum(nil) } func sha256Hex(value []byte) string { sum := sha256.Sum256(value) return hex.EncodeToString(sum[:]) } type removeUserByStrRoomRequest struct { SDKAppID int64 `json:"SdkAppId"` RoomID string `json:"RoomId"` UserIDs []string `json:"UserIds"` } type cloudAPIResponse struct { Response struct { RequestID string `json:"RequestId"` Error *cloudAPIError `json:"Error,omitempty"` } `json:"Response"` } type cloudAPIError struct { Code string `json:"Code"` Message string `json:"Message"` }