feat: add client call feature switches
This commit is contained in:
+1
-1
@@ -358,7 +358,7 @@ func (t *Topic) handleCallInvite(msg *ClientComMessage, asUid types.Uid) {
|
||||
}
|
||||
t.auditCallCreate(t.currentCall, asUid)
|
||||
// Wait for constCallEstablishmentTimeout for the other side to accept the call.
|
||||
t.callEstablishmentTimer.Reset(time.Duration(globals.callEstablishmentTimeout) * time.Second)
|
||||
t.callEstablishmentTimer.Reset(time.Duration(imCallTimeoutSeconds()) * time.Second)
|
||||
}
|
||||
|
||||
// Handles events on existing video call (acceptance, termination, metadata exchange).
|
||||
|
||||
@@ -20,6 +20,8 @@ const (
|
||||
imReasonCallNotFound = "CALL_NOT_FOUND"
|
||||
imReasonCallNotAllowed = "CALL_NOT_ALLOWED"
|
||||
imReasonCallStateInvalid = "CALL_STATE_INVALID"
|
||||
|
||||
imClientFeatureConfigGroup = "im_client_feature"
|
||||
)
|
||||
|
||||
type imCallRTCJoinRequest struct {
|
||||
@@ -189,6 +191,46 @@ func loadIMAgoraConfig() (imAgoraConfig, error) {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func imCallFeatureFlags() (bool, bool) {
|
||||
runtimeEnabled := imCallRuntimeEnabled()
|
||||
if !runtimeEnabled || store.ThirdPartyConfig == nil {
|
||||
return false, false
|
||||
}
|
||||
values, err := store.ThirdPartyConfig.Get(imClientFeatureConfigGroup, "audio_call_enabled", "video_call_enabled")
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
return imTruthy(values["audio_call_enabled"]), imTruthy(values["video_call_enabled"])
|
||||
}
|
||||
|
||||
func imCallRuntimeEnabled() bool {
|
||||
webrtcEnabled := len(globals.iceServers) > 0
|
||||
agoraEnabled := false
|
||||
if config, err := loadIMAgoraConfig(); err == nil {
|
||||
agoraEnabled = config.Enabled
|
||||
}
|
||||
return webrtcEnabled || agoraEnabled
|
||||
}
|
||||
|
||||
func imCallFeatureEnabled() bool {
|
||||
audioCall, videoCall := imCallFeatureFlags()
|
||||
return audioCall || videoCall
|
||||
}
|
||||
|
||||
func imCallTimeoutSeconds() int {
|
||||
if globals.callEstablishmentTimeout > 0 {
|
||||
return globals.callEstablishmentTimeout
|
||||
}
|
||||
if imCallFeatureEnabled() {
|
||||
return defaultCallEstablishmentTimeout
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func imCanCall(allowed bool) bool {
|
||||
return allowed && imCallFeatureEnabled()
|
||||
}
|
||||
|
||||
func findCallRecordForRTCJoin(tenantID types.TenantID, uid types.Uid, body imCallRTCJoinRequest) (*types.CallRecord, error) {
|
||||
if store.Calls == nil {
|
||||
return nil, types.ErrUnsupported
|
||||
|
||||
+9
-4
@@ -1712,6 +1712,7 @@ func serveIMBootstrap(wrt http.ResponseWriter, req *http.Request) {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
audioCallEnabled, videoCallEnabled := imCallFeatureFlags()
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"features": map[string]any{
|
||||
"create_group": true,
|
||||
@@ -1721,11 +1722,15 @@ func serveIMBootstrap(wrt http.ResponseWriter, req *http.Request) {
|
||||
"attendance": true,
|
||||
"message_recall": true,
|
||||
"message_edit": true,
|
||||
"audio_call": audioCallEnabled,
|
||||
"video_call": videoCallEnabled,
|
||||
},
|
||||
"permissions": map[string]any{
|
||||
"create_group": true,
|
||||
"user_search": true,
|
||||
"scan_qr": true,
|
||||
"audio_call": audioCallEnabled,
|
||||
"video_call": videoCallEnabled,
|
||||
},
|
||||
"entry_badges": entryBadges,
|
||||
"entries": entries,
|
||||
@@ -6913,7 +6918,7 @@ func groupMemberProfilePayload(topic string, sub *types.Subscription, user *type
|
||||
data["relationship"] = ctx.Relationship
|
||||
data["capabilities"] = map[string]any{
|
||||
"can_send": ctx.CanSend,
|
||||
"can_call": ctx.CanSend,
|
||||
"can_call": imCanCall(ctx.CanSend),
|
||||
"can_add_friend": ctx.Relationship == types.RelationshipNone,
|
||||
}
|
||||
if ctx.RequestID != "" {
|
||||
@@ -8447,7 +8452,7 @@ func relationshipContextPayload(ctx *types.RelationshipContext) map[string]any {
|
||||
"can_send_text": ctx.CanSend,
|
||||
"can_send_media": ctx.CanSend,
|
||||
"can_send_card": ctx.CanSend,
|
||||
"can_call": ctx.CanSend,
|
||||
"can_call": imCanCall(ctx.CanSend),
|
||||
"can_add_friend": ctx.Relationship == types.RelationshipNone,
|
||||
"can_handle_request": ctx.Relationship == types.RelationshipPendingInbound,
|
||||
},
|
||||
@@ -8506,7 +8511,7 @@ func friendListItemPayload(item types.FriendListItem) map[string]any {
|
||||
"online_visibility": "unknown",
|
||||
"created_at": item.Friendship.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"updated_at": item.Friendship.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"capabilities": map[string]any{"can_send": item.Friendship.Status == types.RelationshipFriend, "can_call": item.Friendship.Status == types.RelationshipFriend},
|
||||
"capabilities": map[string]any{"can_send": item.Friendship.Status == types.RelationshipFriend, "can_call": imCanCall(item.Friendship.Status == types.RelationshipFriend)},
|
||||
"display_name_source": "profile",
|
||||
}
|
||||
if item.Friendship.Remark != "" {
|
||||
@@ -8572,7 +8577,7 @@ func userProfilePayload(user *types.User, ctx *types.RelationshipContext) map[st
|
||||
data["topic"] = ctx.Topic
|
||||
data["capabilities"] = map[string]any{
|
||||
"can_send": ctx.CanSend,
|
||||
"can_call": ctx.CanSend,
|
||||
"can_call": imCanCall(ctx.CanSend),
|
||||
"can_add_friend": ctx.Relationship == types.RelationshipNone,
|
||||
}
|
||||
if ctx.RequestID != "" {
|
||||
|
||||
@@ -2315,6 +2315,7 @@ func TestServeIMBootstrapIncludesHomeCapabilities(t *testing.T) {
|
||||
oldSecurity := store.Security
|
||||
oldWorkbench := store.Workbench
|
||||
oldRelations := store.Relations
|
||||
oldThirdParty := store.ThirdPartyConfig
|
||||
security := &testSecurityStore{
|
||||
realName: &types.RealNameVerification{Status: "verified"},
|
||||
unreadNotifications: 4,
|
||||
@@ -2328,10 +2329,19 @@ func TestServeIMBootstrapIncludesHomeCapabilities(t *testing.T) {
|
||||
store.Security = security
|
||||
store.Relations = relations
|
||||
store.Workbench = workbench
|
||||
store.ThirdPartyConfig = testThirdPartyConfigStore{
|
||||
"agora_enabled": "1",
|
||||
"agora_auth_mode": "app_id",
|
||||
"agora_app_id": "app123",
|
||||
"agora_token_ttl_seconds": "3600",
|
||||
"audio_call_enabled": "1",
|
||||
"video_call_enabled": "1",
|
||||
}
|
||||
defer func() {
|
||||
store.Security = oldSecurity
|
||||
store.Relations = oldRelations
|
||||
store.Workbench = oldWorkbench
|
||||
store.ThirdPartyConfig = oldThirdParty
|
||||
}()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/im/bootstrap", nil)
|
||||
@@ -2347,6 +2357,10 @@ func TestServeIMBootstrapIncludesHomeCapabilities(t *testing.T) {
|
||||
if data["features"] == nil || data["permissions"] == nil || data["system_topic"] == nil || data["message_policy"] == nil {
|
||||
t.Fatalf("bootstrap missing home capabilities: %#v", data)
|
||||
}
|
||||
features := data["features"].(map[string]any)
|
||||
if features["audio_call"] != true || features["video_call"] != true {
|
||||
t.Fatalf("bootstrap call features disabled unexpectedly: %#v", features)
|
||||
}
|
||||
if security.realNameTenant != 7 || security.realNameUser != 42 {
|
||||
t.Fatalf("bootstrap realname lookup scope: tenant=%d uid=%s", security.realNameTenant, security.realNameUser)
|
||||
}
|
||||
|
||||
+2
-2
@@ -898,8 +898,8 @@ func (s *Session) hello(msg *ClientComMessage) {
|
||||
if len(globals.iceServers) > 0 {
|
||||
params["iceServers"] = globals.iceServers
|
||||
}
|
||||
if globals.callEstablishmentTimeout > 0 {
|
||||
params["callTimeout"] = globals.callEstablishmentTimeout
|
||||
if timeout := imCallTimeoutSeconds(); timeout > 0 {
|
||||
params["callTimeout"] = timeout
|
||||
}
|
||||
|
||||
if s.proto == GRPC {
|
||||
|
||||
+1
-1
@@ -1218,7 +1218,7 @@ func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
|
||||
|
||||
isCall := msg.Pub.Head != nil && msg.Pub.Head["webrtc"] != nil
|
||||
if isCall {
|
||||
if len(globals.iceServers) == 0 {
|
||||
if !imCallFeatureEnabled() {
|
||||
msg.sess.queueOut(ErrNotImplementedReply(msg, types.TimeNow()))
|
||||
return
|
||||
}
|
||||
|
||||
+16
-3
@@ -375,9 +375,18 @@ func TestHandleBroadcastCall(t *testing.T) {
|
||||
numUsers := 2
|
||||
helper := TopicTestHelper{}
|
||||
helper.setUp(t, numUsers, types.TopicCatP2P, "p2p-test" /*attach=*/, true)
|
||||
oldThirdParty := store.ThirdPartyConfig
|
||||
globals.iceServers = []iceServer{{Username: "dummy"}}
|
||||
store.ThirdPartyConfig = testThirdPartyConfigStore{
|
||||
"audio_call_enabled": "1",
|
||||
"video_call_enabled": "1",
|
||||
}
|
||||
helper.topic.lastID = 5
|
||||
defer helper.tearDown()
|
||||
defer func() {
|
||||
globals.iceServers = nil
|
||||
store.ThirdPartyConfig = oldThirdParty
|
||||
helper.tearDown()
|
||||
}()
|
||||
helper.mm.EXPECT().Save(types.TenantID(1), gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
Return(&types.MessageSaveResult{SeqId: 1, MarkedReadBySender: true}, nil)
|
||||
|
||||
@@ -401,8 +410,6 @@ func TestHandleBroadcastCall(t *testing.T) {
|
||||
t.Fatal(errorMsgs[0].Ctrl.Text)
|
||||
}
|
||||
|
||||
globals.iceServers = nil
|
||||
|
||||
// Message uid1 -> uid2.
|
||||
for i, m := range helper.results {
|
||||
if i == 0 {
|
||||
@@ -490,9 +497,15 @@ func TestHandleBroadcastCallRequiresStructuredInvite(t *testing.T) {
|
||||
numUsers := 2
|
||||
helper := TopicTestHelper{}
|
||||
helper.setUp(t, numUsers, types.TopicCatP2P, "p2p-test" /*attach=*/, true)
|
||||
oldThirdParty := store.ThirdPartyConfig
|
||||
globals.iceServers = []iceServer{{Username: "dummy"}}
|
||||
store.ThirdPartyConfig = testThirdPartyConfigStore{
|
||||
"audio_call_enabled": "1",
|
||||
"video_call_enabled": "1",
|
||||
}
|
||||
defer func() {
|
||||
globals.iceServers = nil
|
||||
store.ThirdPartyConfig = oldThirdParty
|
||||
helper.tearDown()
|
||||
}()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user