Repository: zulip/zulip

Issue:
Add Zoom support for audio-only calls 
Following #25922, we now have support for audio calls in Zulip, but the Zoom integration doesn't yet support it.

We should figure out what parameters we need to pass when creating a Zoom call to support an "Audio call" button for Zoom users as well.

Tagged as an 8.0 release goal, since we'll want to document the audio calls feature as available for all call providers.


Discussion:

Resolving patch:
diff --git a/version.py b/version.py
--- a/version.py
+++ b/version.py
@@ -33,7 +33,7 @@
 # Changes should be accompanied by documentation explaining what the
 # new level means in api_docs/changelog.md, as well as "**Changes**"
 # entries in the endpoint's documentation in `zulip.yaml`.
-API_FEATURE_LEVEL = 205
+API_FEATURE_LEVEL = 206
 
 # Bump the minor PROVISION_VERSION to indicate that folks should provision
 # only when going from an old version of the code to a newer version. Bump
diff --git a/zerver/views/video_calls.py b/zerver/views/video_calls.py
--- a/zerver/views/video_calls.py
+++ b/zerver/views/video_calls.py
@@ -31,7 +31,7 @@
 from zerver.lib.response import json_success
 from zerver.lib.subdomains import get_subdomain
 from zerver.lib.url_encoding import append_url_query_string
-from zerver.lib.validator import check_dict, check_string
+from zerver.lib.validator import check_bool, check_dict, check_string
 from zerver.models import UserProfile, get_realm
 
 
@@ -142,13 +142,30 @@ def complete_zoom_user_in_realm(
     return render(request, "zerver/close_window.html")
 
 
-def make_zoom_video_call(request: HttpRequest, user: UserProfile) -> HttpResponse:
+@has_request_variables
+def make_zoom_video_call(
+    request: HttpRequest,
+    user: UserProfile,
+    is_video_call: bool = REQ(json_validator=check_bool, default=True),
+) -> HttpResponse:
     oauth = get_zoom_session(user)
     if not oauth.authorized:
         raise InvalidZoomTokenError
 
+    # The meeting host has the ability to configure both their own and
+    # participants' default video on/off state for the meeting. That's
+    # why when creating a meeting, configure the video on/off default
+    # according to the desired call type. Each Zoom user can still have
+    # their own personal setting to not start video by default.
+    payload = {
+        "settings": {
+            "host_video": is_video_call,
+            "participant_video": is_video_call,
+        }
+    }
+
     try:
-        res = oauth.post("https://api.zoom.us/v2/users/me/meetings", json={})
+        res = oauth.post("https://api.zoom.us/v2/users/me/meetings", json=payload)
     except OAuth2Error:
         do_set_zoom_token(user, None)
         raise InvalidZoomTokenError


Test patch:
diff --git a/web/tests/compose_video.test.js b/web/tests/compose_video.test.js
--- a/web/tests/compose_video.test.js
+++ b/web/tests/compose_video.test.js
@@ -132,7 +132,7 @@ test("videos", ({override}) => {
         assert.match(syntax_to_insert, video_link_regex);
     })();
 
-    (function test_zoom_video_link_compose_clicked() {
+    (function test_zoom_video_and_audio_links_compose_clicked() {
         let syntax_to_insert;
         let called = false;
 
@@ -152,9 +152,6 @@ test("videos", ({override}) => {
             called = true;
         });
 
-        const handler = $("body").get_on_handler("click", ".video_link");
-        $("#compose-textarea").val("");
-
         page_params.realm_video_chat_provider = realm_available_video_chat_providers.zoom.id;
         page_params.has_zoom_token = false;
 
@@ -172,10 +169,19 @@ test("videos", ({override}) => {
             return {abort() {}};
         };
 
-        handler(ev);
+        $("#compose-textarea").val("");
+        const video_handler = $("body").get_on_handler("click", ".video_link");
+        video_handler(ev);
         const video_link_regex = /\[translated: Join video call\.]\(example\.zoom\.com\)/;
         assert.ok(called);
         assert.match(syntax_to_insert, video_link_regex);
+
+        $("#compose-textarea").val("");
+        const audio_handler = $("body").get_on_handler("click", ".audio_link");
+        audio_handler(ev);
+        const audio_link_regex = /\[translated: Join audio call\.]\(example\.zoom\.com\)/;
+        assert.ok(called);
+        assert.match(syntax_to_insert, audio_link_regex);
     })();
 
     (function test_bbb_video_link_compose_clicked() {
diff --git a/zerver/tests/test_create_video_call.py b/zerver/tests/test_create_video_call.py
--- a/zerver/tests/test_create_video_call.py
+++ b/zerver/tests/test_create_video_call.py
@@ -36,7 +36,7 @@ def test_register_video_request(self) -> None:
         self.assertEqual(response.status_code, 302)
 
     @responses.activate
-    def test_create_video_request_success(self) -> None:
+    def test_create_zoom_video_and_audio_links(self) -> None:
         responses.add(
             responses.POST,
             "https://zoom.us/oauth/token",
@@ -49,6 +49,7 @@ def test_create_video_request_success(self) -> None:
         )
         self.assertEqual(response.status_code, 200)
 
+        # Test creating a video link
         responses.replace(
             responses.POST,
             "https://zoom.us/oauth/token",
@@ -61,7 +62,32 @@ def test_create_video_request_success(self) -> None:
             json={"join_url": "example.com"},
         )
 
-        response = self.client_post("/json/calls/zoom/create")
+        response = self.client_post("/json/calls/zoom/create", {"is_video_call": "true"})
+        self.assertEqual(
+            responses.calls[-1].request.url,
+            "https://api.zoom.us/v2/users/me/meetings",
+        )
+        self.assertEqual(
+            responses.calls[-1].request.headers["Authorization"],
+            "Bearer newtoken",
+        )
+        json = self.assert_json_success(response)
+        self.assertEqual(json["url"], "example.com")
+
+        # Test creating an audio link
+        responses.replace(
+            responses.POST,
+            "https://zoom.us/oauth/token",
+            json={"access_token": "newtoken", "expires_in": 60},
+        )
+
+        responses.add(
+            responses.POST,
+            "https://api.zoom.us/v2/users/me/meetings",
+            json={"join_url": "example.com"},
+        )
+
+        response = self.client_post("/json/calls/zoom/create", {"is_video_call": "false"})
         self.assertEqual(
             responses.calls[-1].request.url,
             "https://api.zoom.us/v2/users/me/meetings",
@@ -73,6 +99,7 @@ def test_create_video_request_success(self) -> None:
         json = self.assert_json_success(response)
         self.assertEqual(json["url"], "example.com")
 
+        # Test for authentication error
         self.logout()
         self.login_user(self.user)