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.
This commit is contained in:
Danny Su 2023-08-26 17:43:48 -07:00 committed by Tim Abbott
parent 75a654b9ab
commit 66b9c06de6
6 changed files with 81 additions and 18 deletions

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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() {

View File

@ -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)

View File

@ -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