From 66b9c06de63636104bf141fb608afda7856d00eb Mon Sep 17 00:00:00 2001 From: Danny Su Date: Sat, 26 Aug 2023 17:43:48 -0700 Subject: [PATCH] compose: Add support for Zoom audio call This PR implements the audio call feature for Zoom. This is done by explicitly telling Zoom to create a meeting where the host's video and participants' video are off by default. Another key change is that when creating a video call, the host's and participants' video will be on by default. The old code doesn't specify that setting, so meetings actually start with video being off. This new behavior has less work for users to do. They don't have to turn on video when joining a call advertised as "video call". It still respects users' preferences because they can still configure their own personal setting that overrides the meeting defaults. The Zoom API documentation can be found at https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingCreate Fixes #26549. --- api_docs/changelog.md | 6 +++++ version.py | 2 +- web/src/compose.js | 21 +++++++++++------ web/tests/compose_video.test.js | 16 ++++++++----- zerver/tests/test_create_video_call.py | 31 ++++++++++++++++++++++++-- zerver/views/video_calls.py | 23 ++++++++++++++++--- 6 files changed, 81 insertions(+), 18 deletions(-) diff --git a/api_docs/changelog.md b/api_docs/changelog.md index a1c5240933..ed937fc199 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 8.0 +**Feature level 206** + +* `POST /calls/zoom/create`: Added `is_video_call` parameter + controlling whether to request a Zoom meeting that defaults to + having video enabled. + **Feature level 205** * [`POST /register`](/api/register-queue): `streams` field in the response diff --git a/version.py b/version.py index c273b93491..eebc233bc4 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # 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/web/src/compose.js b/web/src/compose.js index ddddd0363b..70f5a85d52 100644 --- a/web/src/compose.js +++ b/web/src/compose.js @@ -89,8 +89,10 @@ export function update_video_chat_button_display() { export function compute_show_audio_chat_button() { const available_providers = page_params.realm_available_video_chat_providers; if ( - available_providers.jitsi_meet && - page_params.realm_video_chat_provider === available_providers.jitsi_meet.id + (available_providers.jitsi_meet && + page_params.realm_video_chat_provider === available_providers.jitsi_meet.id) || + (available_providers.zoom && + page_params.realm_video_chat_provider === available_providers.zoom.id) ) { return true; } @@ -856,21 +858,26 @@ function generate_and_insert_audio_or_video_call_link($target_element, is_audio_ available_providers.zoom && page_params.realm_video_chat_provider === available_providers.zoom.id ) { - if (is_audio_call) { - // TODO: Add support for generating audio-only Zoom calls here. - return; - } abort_video_callbacks(edit_message_id); const key = edit_message_id || ""; + const request = { + is_video_call: !is_audio_call, + }; + const make_zoom_call = () => { video_call_xhrs.set( key, channel.post({ url: "/json/calls/zoom/create", + data: request, success(res) { video_call_xhrs.delete(key); - insert_video_call_url(res.url, $target_textarea); + if (is_audio_call) { + insert_audio_call_url(res.url, $target_textarea); + } else { + insert_video_call_url(res.url, $target_textarea); + } }, error(xhr, status) { video_call_xhrs.delete(key); diff --git a/web/tests/compose_video.test.js b/web/tests/compose_video.test.js index 76af1e9074..27fc177bda 100644 --- 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 index 4eae085cce..ee3e35c179 100644 --- a/zerver/tests/test_create_video_call.py +++ b/zerver/tests/test_create_video_call.py @@ -36,7 +36,7 @@ class TestVideoCall(ZulipTestCase): 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 @@ class TestVideoCall(ZulipTestCase): ) self.assertEqual(response.status_code, 200) + # Test creating a video link responses.replace( responses.POST, "https://zoom.us/oauth/token", @@ -61,7 +62,7 @@ class TestVideoCall(ZulipTestCase): 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", @@ -73,6 +74,32 @@ class TestVideoCall(ZulipTestCase): 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", + ) + self.assertEqual( + responses.calls[-1].request.headers["Authorization"], + "Bearer newtoken", + ) + json = self.assert_json_success(response) + self.assertEqual(json["url"], "example.com") + + # Test for authentication error self.logout() self.login_user(self.user) diff --git a/zerver/views/video_calls.py b/zerver/views/video_calls.py index c831edf21e..4d26681794 100644 --- a/zerver/views/video_calls.py +++ b/zerver/views/video_calls.py @@ -31,7 +31,7 @@ from zerver.lib.request import REQ, has_request_variables 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