mirror of https://github.com/zulip/zulip.git
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:
parent
75a654b9ab
commit
66b9c06de6
|
@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
|
||||||
|
|
||||||
## Changes in Zulip 8.0
|
## 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**
|
**Feature level 205**
|
||||||
|
|
||||||
* [`POST /register`](/api/register-queue): `streams` field in the response
|
* [`POST /register`](/api/register-queue): `streams` field in the response
|
||||||
|
|
|
@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
||||||
# Changes should be accompanied by documentation explaining what the
|
# Changes should be accompanied by documentation explaining what the
|
||||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
# 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
|
# 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
|
# only when going from an old version of the code to a newer version. Bump
|
||||||
|
|
|
@ -89,8 +89,10 @@ export function update_video_chat_button_display() {
|
||||||
export function compute_show_audio_chat_button() {
|
export function compute_show_audio_chat_button() {
|
||||||
const available_providers = page_params.realm_available_video_chat_providers;
|
const available_providers = page_params.realm_available_video_chat_providers;
|
||||||
if (
|
if (
|
||||||
available_providers.jitsi_meet &&
|
(available_providers.jitsi_meet &&
|
||||||
page_params.realm_video_chat_provider === available_providers.jitsi_meet.id
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -856,21 +858,26 @@ function generate_and_insert_audio_or_video_call_link($target_element, is_audio_
|
||||||
available_providers.zoom &&
|
available_providers.zoom &&
|
||||||
page_params.realm_video_chat_provider === available_providers.zoom.id
|
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);
|
abort_video_callbacks(edit_message_id);
|
||||||
const key = edit_message_id || "";
|
const key = edit_message_id || "";
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
is_video_call: !is_audio_call,
|
||||||
|
};
|
||||||
|
|
||||||
const make_zoom_call = () => {
|
const make_zoom_call = () => {
|
||||||
video_call_xhrs.set(
|
video_call_xhrs.set(
|
||||||
key,
|
key,
|
||||||
channel.post({
|
channel.post({
|
||||||
url: "/json/calls/zoom/create",
|
url: "/json/calls/zoom/create",
|
||||||
|
data: request,
|
||||||
success(res) {
|
success(res) {
|
||||||
video_call_xhrs.delete(key);
|
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) {
|
error(xhr, status) {
|
||||||
video_call_xhrs.delete(key);
|
video_call_xhrs.delete(key);
|
||||||
|
|
|
@ -132,7 +132,7 @@ test("videos", ({override}) => {
|
||||||
assert.match(syntax_to_insert, video_link_regex);
|
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 syntax_to_insert;
|
||||||
let called = false;
|
let called = false;
|
||||||
|
|
||||||
|
@ -152,9 +152,6 @@ test("videos", ({override}) => {
|
||||||
called = true;
|
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.realm_video_chat_provider = realm_available_video_chat_providers.zoom.id;
|
||||||
page_params.has_zoom_token = false;
|
page_params.has_zoom_token = false;
|
||||||
|
|
||||||
|
@ -172,10 +169,19 @@ test("videos", ({override}) => {
|
||||||
return {abort() {}};
|
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\)/;
|
const video_link_regex = /\[translated: Join video call\.]\(example\.zoom\.com\)/;
|
||||||
assert.ok(called);
|
assert.ok(called);
|
||||||
assert.match(syntax_to_insert, video_link_regex);
|
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() {
|
(function test_bbb_video_link_compose_clicked() {
|
||||||
|
|
|
@ -36,7 +36,7 @@ class TestVideoCall(ZulipTestCase):
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_create_video_request_success(self) -> None:
|
def test_create_zoom_video_and_audio_links(self) -> None:
|
||||||
responses.add(
|
responses.add(
|
||||||
responses.POST,
|
responses.POST,
|
||||||
"https://zoom.us/oauth/token",
|
"https://zoom.us/oauth/token",
|
||||||
|
@ -49,6 +49,7 @@ class TestVideoCall(ZulipTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Test creating a video link
|
||||||
responses.replace(
|
responses.replace(
|
||||||
responses.POST,
|
responses.POST,
|
||||||
"https://zoom.us/oauth/token",
|
"https://zoom.us/oauth/token",
|
||||||
|
@ -61,7 +62,7 @@ class TestVideoCall(ZulipTestCase):
|
||||||
json={"join_url": "example.com"},
|
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(
|
self.assertEqual(
|
||||||
responses.calls[-1].request.url,
|
responses.calls[-1].request.url,
|
||||||
"https://api.zoom.us/v2/users/me/meetings",
|
"https://api.zoom.us/v2/users/me/meetings",
|
||||||
|
@ -73,6 +74,32 @@ class TestVideoCall(ZulipTestCase):
|
||||||
json = self.assert_json_success(response)
|
json = self.assert_json_success(response)
|
||||||
self.assertEqual(json["url"], "example.com")
|
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.logout()
|
||||||
self.login_user(self.user)
|
self.login_user(self.user)
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ from zerver.lib.request import REQ, has_request_variables
|
||||||
from zerver.lib.response import json_success
|
from zerver.lib.response import json_success
|
||||||
from zerver.lib.subdomains import get_subdomain
|
from zerver.lib.subdomains import get_subdomain
|
||||||
from zerver.lib.url_encoding import append_url_query_string
|
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
|
from zerver.models import UserProfile, get_realm
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,13 +142,30 @@ def complete_zoom_user_in_realm(
|
||||||
return render(request, "zerver/close_window.html")
|
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)
|
oauth = get_zoom_session(user)
|
||||||
if not oauth.authorized:
|
if not oauth.authorized:
|
||||||
raise InvalidZoomTokenError
|
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:
|
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:
|
except OAuth2Error:
|
||||||
do_set_zoom_token(user, None)
|
do_set_zoom_token(user, None)
|
||||||
raise InvalidZoomTokenError
|
raise InvalidZoomTokenError
|
||||||
|
|
Loading…
Reference in New Issue