485 lines
18 KiB
Go
485 lines
18 KiB
Go
package smoke_test
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"hyapp/pkg/tencentim"
|
||
)
|
||
|
||
func TestRoomRPSLocalHTTPFlowWithRealTencentIM(t *testing.T) {
|
||
if os.Getenv("ROOM_RPS_REAL_IM_FLOW_TEST") != "1" {
|
||
t.Skip("set ROOM_RPS_REAL_IM_FLOW_TEST=1 to run the local room-rps HTTP flow with real Tencent IM")
|
||
}
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||
defer cancel()
|
||
|
||
cfg := roomRPSRealIMConfigFromEnv(t)
|
||
imClient, err := tencentim.NewRESTClient(cfg)
|
||
if err != nil {
|
||
t.Fatalf("new real Tencent IM client failed: %v", err)
|
||
}
|
||
|
||
baseURL := strings.TrimRight(envOr("ROOM_RPS_BASE_URL", "http://127.0.0.1:13000/api/v1"), "/")
|
||
appCode := envOr("ROOM_RPS_APP_CODE", "lalu")
|
||
roomID := envOr("ROOM_RPS_ROOM_ID", fmt.Sprintf("room-rps-real-im-%d", time.Now().UnixMilli()))
|
||
imGroupID := envOr("ROOM_RPS_IM_GROUP_ID", roomID)
|
||
giftID := positiveInt64EnvOr(t, "ROOM_RPS_GIFT_ID", 10001)
|
||
httpClient := &http.Client{Timeout: 15 * time.Second}
|
||
|
||
initiator := quickCreateRoomRPSSmokeAccount(t, ctx, httpClient, baseURL, appCode, "room-rps-initiator")
|
||
challenger := quickCreateRoomRPSSmokeAccount(t, ctx, httpClient, baseURL, appCode, "room-rps-challenger")
|
||
if initiator.UserID == challenger.UserID {
|
||
t.Fatalf("quick-created users must be different: initiator=%+v challenger=%+v", initiator, challenger)
|
||
}
|
||
|
||
// 真实 IM 是这个 smoke 的硬前置:先确保房间群存在并导入两个账号,避免后续 HTTP 成功但 IM 配置其实不可用。
|
||
if err := imClient.EnsureGroup(ctx, tencentim.GroupSpec{
|
||
GroupID: imGroupID,
|
||
Name: "room rps real im smoke " + roomID,
|
||
Type: tencentim.DefaultGroupType,
|
||
ApplyJoinOption: "FreeAccess",
|
||
}); err != nil {
|
||
t.Fatalf("ensure real Tencent IM room group failed: %v", err)
|
||
}
|
||
if shouldCleanupRoomRPSIMGroup() {
|
||
if !strings.HasPrefix(imGroupID, "room-rps-real-im-") {
|
||
t.Fatalf("ROOM_RPS_CLEANUP_IM_GROUP=1 only allows group id with room-rps-real-im- prefix, got %q", imGroupID)
|
||
}
|
||
t.Cleanup(func() {
|
||
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||
defer cleanupCancel()
|
||
if err := imClient.DestroyGroup(cleanupCtx, imGroupID); err != nil {
|
||
t.Errorf("destroy real Tencent IM room-rps smoke group failed: %v", err)
|
||
}
|
||
})
|
||
}
|
||
importRoomRPSIMAccount(t, ctx, imClient, initiator.UserID, initiator.Username)
|
||
importRoomRPSIMAccount(t, ctx, imClient, challenger.UserID, challenger.Username)
|
||
publishRoomRPSIMProbe(t, ctx, imClient, imGroupID, roomID)
|
||
|
||
// /im/usersig 是 App 真正登录腾讯 IM SDK 前要调用的 gateway 接口;这里校验本地 gateway 和真实腾讯 IM SDKAppID 使用同一套配置。
|
||
initiatorSig := requestRoomRPSUserSig(t, ctx, httpClient, baseURL, appCode, initiator.AccessToken, cfg.SDKAppID, initiator.UserID)
|
||
challengerSig := requestRoomRPSUserSig(t, ctx, httpClient, baseURL, appCode, challenger.AccessToken, cfg.SDKAppID, challenger.UserID)
|
||
if initiatorSig.UserID == challengerSig.UserID {
|
||
t.Fatalf("initiator and challenger must be different IM identifiers: %s", initiatorSig.UserID)
|
||
}
|
||
|
||
config := requestRoomRPSConfig(t, ctx, httpClient, baseURL, appCode, initiator.AccessToken)
|
||
if strings.ToLower(strings.TrimSpace(config.Config.Status)) != "active" {
|
||
t.Fatalf("room-rps config must be open by default, got=%+v", config.Config)
|
||
}
|
||
assertRoomRPSGiftConfigured(t, config, giftID)
|
||
|
||
listBefore := requestRoomRPSChallengeList(t, ctx, httpClient, baseURL, appCode, initiator.AccessToken, roomID)
|
||
t.Logf("room-rps list before create: count=%d next_cursor=%q", len(listBefore.Challenges), listBefore.NextCursor)
|
||
|
||
created := createRoomRPSChallenge(t, ctx, httpClient, baseURL, appCode, initiator.AccessToken, roomID, giftID, "rock")
|
||
if created.Challenge.ChallengeID == "" {
|
||
t.Fatalf("created room-rps challenge must contain challenge_id: %+v", created.Challenge)
|
||
}
|
||
if created.Challenge.Initiator.UserID != strconv.FormatInt(initiator.UserID, 10) {
|
||
t.Fatalf("created challenge initiator mismatch: %+v", created.Challenge.Initiator)
|
||
}
|
||
if strings.TrimSpace(created.Challenge.Initiator.User.Nickname) == "" || strings.TrimSpace(created.Challenge.Initiator.User.Avatar) == "" {
|
||
t.Fatalf("created challenge must include initiator nickname/avatar for IM UI: %+v", created.Challenge.Initiator.User)
|
||
}
|
||
|
||
accepted := acceptRoomRPSChallenge(t, ctx, httpClient, baseURL, appCode, challenger.AccessToken, created.Challenge.ChallengeID, "paper")
|
||
if accepted.Challenge.Challenger.UserID != strconv.FormatInt(challenger.UserID, 10) {
|
||
t.Fatalf("accepted challenge challenger mismatch: %+v", accepted.Challenge.Challenger)
|
||
}
|
||
if strings.TrimSpace(accepted.Challenge.Challenger.User.Nickname) == "" || strings.TrimSpace(accepted.Challenge.Challenger.User.Avatar) == "" {
|
||
t.Fatalf("accepted challenge must include challenger nickname/avatar for IM UI: %+v", accepted.Challenge.Challenger.User)
|
||
}
|
||
|
||
detail := getRoomRPSChallenge(t, ctx, httpClient, baseURL, appCode, challenger.AccessToken, created.Challenge.ChallengeID)
|
||
if detail.Challenge.ChallengeID != created.Challenge.ChallengeID {
|
||
t.Fatalf("detail challenge id mismatch: got=%q want=%q", detail.Challenge.ChallengeID, created.Challenge.ChallengeID)
|
||
}
|
||
if detail.Challenge.Initiator.User.Nickname == "" || detail.Challenge.Challenger.User.Nickname == "" {
|
||
t.Fatalf("detail must preserve both player display profiles: %+v", detail.Challenge)
|
||
}
|
||
|
||
t.Logf("room-rps real HTTP + IM smoke passed: base_url=%s room_id=%s im_group_id=%s challenge_id=%s", baseURL, roomID, imGroupID, created.Challenge.ChallengeID)
|
||
}
|
||
|
||
type roomRPSEnvelope struct {
|
||
Code string `json:"code"`
|
||
Message string `json:"message"`
|
||
RequestID string `json:"request_id"`
|
||
Data json.RawMessage `json:"data"`
|
||
}
|
||
|
||
type roomRPSUserSigData struct {
|
||
SDKAppID int64 `json:"sdk_app_id"`
|
||
UserID string `json:"user_id"`
|
||
UserSig string `json:"user_sig"`
|
||
ExpireAtMS int64 `json:"expire_at_ms"`
|
||
}
|
||
|
||
type roomRPSQuickCreateData struct {
|
||
UID string `json:"uid"`
|
||
Account string `json:"account"`
|
||
AccessToken string `json:"access_token"`
|
||
RefreshToken string `json:"refresh_token"`
|
||
PasswordSet bool `json:"password_set"`
|
||
Token roomRPSAuthToken `json:"token"`
|
||
Username string `json:"-"`
|
||
UserID int64 `json:"-"`
|
||
}
|
||
|
||
type roomRPSAuthToken struct {
|
||
UserID string `json:"user_id"`
|
||
AccessToken string `json:"access_token"`
|
||
}
|
||
|
||
type roomRPSConfigData struct {
|
||
Config roomRPSConfig `json:"config"`
|
||
ServerTimeMS int64 `json:"server_time_ms"`
|
||
}
|
||
|
||
type roomRPSConfig struct {
|
||
Status string `json:"status"`
|
||
ChallengeTimeoutMS int64 `json:"challenge_timeout_ms"`
|
||
RevealCountdownMS int64 `json:"reveal_countdown_ms"`
|
||
StakeGifts []roomRPSStakeGift `json:"stake_gifts"`
|
||
}
|
||
|
||
type roomRPSStakeGift struct {
|
||
GiftID string `json:"gift_id"`
|
||
GiftIDNumber int64 `json:"gift_id_number"`
|
||
Enabled bool `json:"enabled"`
|
||
SortOrder int32 `json:"sort_order"`
|
||
}
|
||
|
||
type roomRPSChallengeListData struct {
|
||
Challenges []roomRPSChallenge `json:"challenges"`
|
||
NextCursor string `json:"next_cursor"`
|
||
PageSize int32 `json:"page_size"`
|
||
ServerTimeMS int64 `json:"server_time_ms"`
|
||
}
|
||
|
||
type roomRPSChallengeData struct {
|
||
Challenge roomRPSChallenge `json:"challenge"`
|
||
ServerTimeMS int64 `json:"server_time_ms"`
|
||
}
|
||
|
||
type roomRPSChallenge struct {
|
||
ChallengeID string `json:"challenge_id"`
|
||
RoomID string `json:"room_id"`
|
||
Status string `json:"status"`
|
||
StakeGiftID string `json:"stake_gift_id"`
|
||
StakeGiftIDNum int64 `json:"stake_gift_id_number"`
|
||
Initiator roomRPSPlayer `json:"initiator"`
|
||
Challenger roomRPSPlayer `json:"challenger"`
|
||
WinnerUserID string `json:"winner_user_id"`
|
||
SettlementStatus string `json:"settlement_status"`
|
||
}
|
||
|
||
type roomRPSPlayer struct {
|
||
UserID string `json:"user_id"`
|
||
UserIDNumber int64 `json:"user_id_number"`
|
||
Gesture string `json:"gesture"`
|
||
Result string `json:"result"`
|
||
User roomRPSDisplayUser `json:"user"`
|
||
}
|
||
|
||
type roomRPSDisplayUser struct {
|
||
UserID string `json:"user_id"`
|
||
DisplayUserID string `json:"display_user_id"`
|
||
Nickname string `json:"nickname"`
|
||
Avatar string `json:"avatar"`
|
||
}
|
||
|
||
func quickCreateRoomRPSSmokeAccount(t *testing.T, ctx context.Context, client *http.Client, baseURL string, appCode string, role string) roomRPSQuickCreateData {
|
||
t.Helper()
|
||
|
||
nowMS := time.Now().UnixMilli()
|
||
username := fmt.Sprintf("%s-%d", role, nowMS)
|
||
body := map[string]any{
|
||
"password": "room-rps-smoke-pass-123",
|
||
"username": username,
|
||
"avatar": fmt.Sprintf("https://cdn.example.com/%s.png", role),
|
||
"gender": "unknown",
|
||
"country": "SG",
|
||
"device_id": fmt.Sprintf("%s-device-%d", role, nowMS),
|
||
"device": "codex-smoke",
|
||
"os_version": "local",
|
||
"birth": "2000-01-01",
|
||
"app_version": "1.0.0",
|
||
"build_number": "1",
|
||
"source": "room_rps_real_im_smoke",
|
||
"install_channel": "codex",
|
||
"platform": "android",
|
||
"language": "en",
|
||
"timezone": "Asia/Shanghai",
|
||
}
|
||
var data roomRPSQuickCreateData
|
||
doRoomRPSJSON(t, ctx, client, http.MethodPost, baseURL+"/auth/account/quick-create", appCode, "", body, &data)
|
||
data.Username = username
|
||
userID, err := strconv.ParseInt(strings.TrimSpace(data.Token.UserID), 10, 64)
|
||
if err != nil || userID <= 0 || strings.TrimSpace(data.AccessToken) == "" || !data.PasswordSet {
|
||
t.Fatalf("quick-create account response invalid: %+v", data)
|
||
}
|
||
data.UserID = userID
|
||
return data
|
||
}
|
||
|
||
func requestRoomRPSUserSig(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token string, sdkAppID int64, userID int64) roomRPSUserSigData {
|
||
t.Helper()
|
||
|
||
var data roomRPSUserSigData
|
||
doRoomRPSJSON(t, ctx, client, http.MethodGet, baseURL+"/im/usersig", appCode, token, nil, &data)
|
||
if data.SDKAppID != sdkAppID {
|
||
t.Fatalf("usersig sdk_app_id mismatch: got=%d want=%d data=%+v", data.SDKAppID, sdkAppID, data)
|
||
}
|
||
if data.UserID != strconv.FormatInt(userID, 10) || strings.TrimSpace(data.UserSig) == "" || data.ExpireAtMS <= time.Now().UnixMilli() {
|
||
t.Fatalf("usersig response mismatch for user %d: %+v", userID, data)
|
||
}
|
||
return data
|
||
}
|
||
|
||
func requestRoomRPSConfig(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token string) roomRPSConfigData {
|
||
t.Helper()
|
||
|
||
var data roomRPSConfigData
|
||
doRoomRPSJSON(t, ctx, client, http.MethodGet, baseURL+"/games/room-rps/config", appCode, token, nil, &data)
|
||
if data.Config.Status == "" {
|
||
t.Fatalf("room-rps config response is empty: %+v", data)
|
||
}
|
||
return data
|
||
}
|
||
|
||
func requestRoomRPSChallengeList(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token, roomID string) roomRPSChallengeListData {
|
||
t.Helper()
|
||
|
||
var data roomRPSChallengeListData
|
||
doRoomRPSJSON(t, ctx, client, http.MethodGet, baseURL+"/games/room-rps/challenges?room_id="+urlQueryEscape(roomID), appCode, token, nil, &data)
|
||
return data
|
||
}
|
||
|
||
func createRoomRPSChallenge(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token, roomID string, giftID int64, gesture string) roomRPSChallengeData {
|
||
t.Helper()
|
||
|
||
body := map[string]any{"room_id": roomID, "gift_id": giftID, "gesture": gesture}
|
||
var data roomRPSChallengeData
|
||
doRoomRPSJSON(t, ctx, client, http.MethodPost, baseURL+"/games/room-rps/challenges/create", appCode, token, body, &data)
|
||
return data
|
||
}
|
||
|
||
func acceptRoomRPSChallenge(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token, challengeID string, gesture string) roomRPSChallengeData {
|
||
t.Helper()
|
||
|
||
body := map[string]any{"gesture": gesture}
|
||
var data roomRPSChallengeData
|
||
doRoomRPSJSON(t, ctx, client, http.MethodPost, baseURL+"/games/room-rps/challenges/"+urlPathEscape(challengeID)+"/accept", appCode, token, body, &data)
|
||
return data
|
||
}
|
||
|
||
func getRoomRPSChallenge(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token, challengeID string) roomRPSChallengeData {
|
||
t.Helper()
|
||
|
||
var data roomRPSChallengeData
|
||
doRoomRPSJSON(t, ctx, client, http.MethodGet, baseURL+"/games/room-rps/challenges/"+urlPathEscape(challengeID), appCode, token, nil, &data)
|
||
return data
|
||
}
|
||
|
||
func doRoomRPSJSON(t *testing.T, ctx context.Context, client *http.Client, method string, url string, appCode string, token string, body any, out any) {
|
||
t.Helper()
|
||
|
||
var payload io.Reader
|
||
if body != nil {
|
||
raw, err := json.Marshal(body)
|
||
if err != nil {
|
||
t.Fatalf("marshal %s %s body failed: %v", method, url, err)
|
||
}
|
||
payload = bytes.NewReader(raw)
|
||
}
|
||
req, err := http.NewRequestWithContext(ctx, method, url, payload)
|
||
if err != nil {
|
||
t.Fatalf("build %s %s request failed: %v", method, url, err)
|
||
}
|
||
if strings.TrimSpace(token) != "" {
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
}
|
||
req.Header.Set("X-App-Code", appCode)
|
||
req.Header.Set("X-App-Language", "en")
|
||
if body != nil {
|
||
req.Header.Set("Content-Type", "application/json")
|
||
}
|
||
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
t.Fatalf("%s %s failed: %v", method, url, err)
|
||
}
|
||
defer resp.Body.Close()
|
||
raw, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
t.Fatalf("read %s %s response failed: %v", method, url, err)
|
||
}
|
||
|
||
var envelope roomRPSEnvelope
|
||
if err := json.Unmarshal(raw, &envelope); err != nil {
|
||
t.Fatalf("decode %s %s envelope failed: status=%d body=%s err=%v", method, url, resp.StatusCode, string(raw), err)
|
||
}
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 || envelope.Code != "OK" {
|
||
t.Fatalf("%s %s returned non-OK: status=%d code=%s message=%s request_id=%s body=%s", method, url, resp.StatusCode, envelope.Code, envelope.Message, envelope.RequestID, string(raw))
|
||
}
|
||
if out != nil {
|
||
if len(envelope.Data) == 0 {
|
||
t.Fatalf("%s %s returned empty data: request_id=%s", method, url, envelope.RequestID)
|
||
}
|
||
if err := json.Unmarshal(envelope.Data, out); err != nil {
|
||
t.Fatalf("decode %s %s data failed: request_id=%s data=%s err=%v", method, url, envelope.RequestID, string(envelope.Data), err)
|
||
}
|
||
}
|
||
}
|
||
|
||
func assertRoomRPSGiftConfigured(t *testing.T, data roomRPSConfigData, giftID int64) {
|
||
t.Helper()
|
||
|
||
for _, gift := range data.Config.StakeGifts {
|
||
if gift.GiftIDNumber == giftID || gift.GiftID == strconv.FormatInt(giftID, 10) {
|
||
if !gift.Enabled {
|
||
t.Fatalf("ROOM_RPS_GIFT_ID is configured but disabled: %+v", gift)
|
||
}
|
||
return
|
||
}
|
||
}
|
||
t.Fatalf("ROOM_RPS_GIFT_ID=%d not found in room-rps config: %+v", giftID, data.Config.StakeGifts)
|
||
}
|
||
|
||
func publishRoomRPSIMProbe(t *testing.T, ctx context.Context, client *tencentim.RESTClient, groupID string, roomID string) {
|
||
t.Helper()
|
||
|
||
nowMS := time.Now().UTC().UnixMilli()
|
||
eventID := fmt.Sprintf("room_rps_real_im_probe:%d", nowMS)
|
||
payload, err := json.Marshal(map[string]any{
|
||
"event_id": eventID,
|
||
"event_type": "room_rps_real_im_probe",
|
||
"room_id": roomID,
|
||
"challenge_id": "",
|
||
"server_time_ms": nowMS,
|
||
"challenge": map[string]any{
|
||
"room_id": roomID,
|
||
"status": "probe",
|
||
},
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("marshal real IM probe failed: %v", err)
|
||
}
|
||
|
||
// 这里真的调用腾讯云 IM REST 发送 TIMCustomElem;成功表示本地配置、admin UserSig、群 ID 和消息 payload 都被腾讯服务端接受。
|
||
if err := client.PublishGroupCustomMessage(ctx, tencentim.CustomGroupMessage{
|
||
GroupID: groupID,
|
||
EventID: eventID,
|
||
Desc: "room_rps_real_im_probe",
|
||
Ext: "room_rps",
|
||
PayloadJSON: payload,
|
||
}); err != nil {
|
||
t.Fatalf("publish real Tencent IM room-rps probe failed: %v", err)
|
||
}
|
||
}
|
||
|
||
func importRoomRPSIMAccount(t *testing.T, ctx context.Context, client *tencentim.RESTClient, userID int64, nick string) {
|
||
t.Helper()
|
||
|
||
if err := client.ImportAccount(ctx, tencentim.AccountProfile{
|
||
UserID: userID,
|
||
Nick: nick,
|
||
FaceURL: "https://cdn.example.com/room-rps-smoke.png",
|
||
}); err != nil {
|
||
t.Fatalf("import real Tencent IM account user_id=%d failed: %v", userID, err)
|
||
}
|
||
}
|
||
|
||
func roomRPSRealIMConfigFromEnv(t *testing.T) tencentim.RESTConfig {
|
||
t.Helper()
|
||
|
||
sdkAppID := requiredPositiveInt64Env(t, "TENCENT_IM_SDK_APP_ID")
|
||
ttl := time.Hour
|
||
if rawTTL := strings.TrimSpace(os.Getenv("TENCENT_IM_ADMIN_USERSIG_TTL")); rawTTL != "" {
|
||
parsed, err := time.ParseDuration(rawTTL)
|
||
if err != nil || parsed <= 0 {
|
||
t.Fatalf("TENCENT_IM_ADMIN_USERSIG_TTL is invalid")
|
||
}
|
||
ttl = parsed
|
||
}
|
||
return tencentim.RESTConfig{
|
||
SDKAppID: sdkAppID,
|
||
SecretKey: requiredRoomRPSEnv(t, "TENCENT_IM_SECRET_KEY"),
|
||
AdminIdentifier: requiredRoomRPSEnv(t, "TENCENT_IM_ADMIN_IDENTIFIER"),
|
||
AdminUserSigTTL: ttl,
|
||
Endpoint: envOr("TENCENT_IM_ENDPOINT", tencentim.DefaultEndpoint),
|
||
GroupType: tencentim.DefaultGroupType,
|
||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||
}
|
||
}
|
||
|
||
func requiredRoomRPSEnv(t *testing.T, key string) string {
|
||
t.Helper()
|
||
|
||
value := strings.TrimSpace(os.Getenv(key))
|
||
if value == "" {
|
||
t.Fatalf("%s is required for room-rps real IM flow test", key)
|
||
}
|
||
return value
|
||
}
|
||
|
||
func requiredPositiveInt64Env(t *testing.T, key string) int64 {
|
||
t.Helper()
|
||
|
||
value, err := strconv.ParseInt(requiredRoomRPSEnv(t, key), 10, 64)
|
||
if err != nil || value <= 0 {
|
||
t.Fatalf("%s must be a positive int64", key)
|
||
}
|
||
return value
|
||
}
|
||
|
||
func positiveInt64EnvOr(t *testing.T, key string, fallback int64) int64 {
|
||
t.Helper()
|
||
|
||
value := strings.TrimSpace(os.Getenv(key))
|
||
if value == "" {
|
||
return fallback
|
||
}
|
||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||
if err != nil || parsed <= 0 {
|
||
t.Fatalf("%s must be a positive int64", key)
|
||
}
|
||
return parsed
|
||
}
|
||
|
||
func envOr(key string, fallback string) string {
|
||
value := strings.TrimSpace(os.Getenv(key))
|
||
if value == "" {
|
||
return fallback
|
||
}
|
||
return value
|
||
}
|
||
|
||
func shouldCleanupRoomRPSIMGroup() bool {
|
||
raw := strings.TrimSpace(os.Getenv("ROOM_RPS_CLEANUP_IM_GROUP"))
|
||
return raw == "" || raw == "1"
|
||
}
|
||
|
||
func urlQueryEscape(value string) string {
|
||
return url.QueryEscape(value)
|
||
}
|
||
|
||
func urlPathEscape(value string) string {
|
||
return url.PathEscape(value)
|
||
}
|