mirror of https://github.com/zulip/zulip.git
calls: Add Big Blue Button as a Video Call Provider.
Big Blue Button needs an API secret so communication to creating a room has to be done server side. Fixes #14763.
This commit is contained in:
parent
077c741ef4
commit
a389c7390d
|
@ -64,6 +64,7 @@ in bursts.
|
|||
- Added server support for creating an account from mobile/terminal apps.
|
||||
- The Zulip desktop apps now do social authentication (Google, GitHub,
|
||||
etc.) via an external browser.
|
||||
- Added support for BigBlueButton as video chat provider.
|
||||
- Added support for setting an organization-wide default language for
|
||||
code blocks.
|
||||
- Added an API endpoint for fetching a single user.
|
||||
|
|
|
@ -947,6 +947,10 @@ run_test('initialize', () => {
|
|||
id: 3,
|
||||
name: "Zoom",
|
||||
},
|
||||
big_blue_button: {
|
||||
id: 4,
|
||||
name: "Big Blue Button",
|
||||
},
|
||||
};
|
||||
|
||||
page_params.realm_video_chat_provider =
|
||||
|
@ -1463,6 +1467,18 @@ run_test('on_events', () => {
|
|||
video_link_regex = /\[Click to join video call\]\(example\.zoom\.com\)/;
|
||||
assert(video_link_regex.test(syntax_to_insert));
|
||||
|
||||
page_params.realm_video_chat_provider =
|
||||
page_params.realm_available_video_chat_providers.big_blue_button.id;
|
||||
|
||||
channel.get = function (options) {
|
||||
assert(options.url === '/json/calls/bigbluebutton/create');
|
||||
options.success({ url: '/calls/bigbluebutton/join?meeting_id=%22zulip-1%22&password=%22AAAAAAAAAA%22&checksum=%2232702220bff2a22a44aee72e96cfdb4c4091752e%22' });
|
||||
};
|
||||
|
||||
handler(ev);
|
||||
video_link_regex = /\[Click to join video call\]\(\/calls\/bigbluebutton\/join\?meeting_id=%22zulip-1%22&password=%22AAAAAAAAAA%22&checksum=%2232702220bff2a22a44aee72e96cfdb4c4091752e%22\)/;
|
||||
assert(video_link_regex.test(syntax_to_insert));
|
||||
|
||||
}());
|
||||
|
||||
(function test_markdown_preview_compose_clicked() {
|
||||
|
|
|
@ -759,6 +759,10 @@ run_test('set_up', () => {
|
|||
id: 3,
|
||||
name: "Zoom",
|
||||
},
|
||||
big_blue_button: {
|
||||
id: 4,
|
||||
name: "Big Blue Button",
|
||||
},
|
||||
};
|
||||
simulate_tip_box();
|
||||
|
||||
|
|
|
@ -1122,6 +1122,15 @@ exports.initialize = function () {
|
|||
"width=800,height=500,noopener,noreferrer"
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
page_params.realm_video_chat_provider === available_providers.big_blue_button.id) {
|
||||
|
||||
channel.get({
|
||||
url: '/json/calls/bigbluebutton/create',
|
||||
success: function (response) {
|
||||
insert_video_call_url(response.url, target_textarea);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
video_call_link = page_params.jitsi_server_url + "/" + video_call_id;
|
||||
insert_video_call_url(video_call_link, target_textarea);
|
||||
|
|
|
@ -238,7 +238,7 @@
|
|||
<h3>VIDEO CALLS</h3>
|
||||
<p>
|
||||
Create and join video calls with a single click. Powered
|
||||
by your choice of Jitsi Meet or Zoom.
|
||||
by your choice of Zoom, Jitsi Meet or Big Blue Button.
|
||||
</p>
|
||||
</a>
|
||||
<a class="feature-block" href="/help/configure-authentication-methods" target="_blank" rel="noopener noreferrer">
|
||||
|
|
|
@ -37,6 +37,12 @@ Meet](https://meet.jit.si/).
|
|||
You can also use Zulip with Jitsi Meet on-premise; to configure this,
|
||||
just set `JITSI_SERVER_URL` in `/etc/zulip/settings.py`.
|
||||
|
||||
{tab|bigbluebutton}
|
||||
|
||||
In order to use Big Blue Button as the video call provider, you need
|
||||
to first configure the `BIG_BLUE_BUTTON_URL` setting in
|
||||
`/etc/zulip/settings.py`.
|
||||
|
||||
{tab|zoom}
|
||||
|
||||
Zulip supports Zoom as the video chat provider using an OAuth
|
||||
|
|
|
@ -66,6 +66,7 @@ IGNORED_PHRASES = [
|
|||
r"Emoji One",
|
||||
r"mailinator.com",
|
||||
r"HQ",
|
||||
r"Big Blue Button",
|
||||
# Code things
|
||||
r".zuliprc",
|
||||
r"__\w+\.\w+__",
|
||||
|
|
|
@ -67,6 +67,7 @@ TAB_DISPLAY_NAMES = {
|
|||
|
||||
'zoom': 'Zoom (experimental)',
|
||||
'jitsi-meet': 'Jitsi Meet',
|
||||
'bigbluebutton': 'Big Blue Button',
|
||||
'disable': 'Disabled',
|
||||
|
||||
'chrome': 'Chrome',
|
||||
|
|
|
@ -362,11 +362,19 @@ class Realm(models.Model):
|
|||
# ID 2 was used for the now-deleted Google Hangouts.
|
||||
# ID 3 reserved for optional Zoom, see below.
|
||||
}
|
||||
|
||||
if settings.VIDEO_ZOOM_CLIENT_ID is not None and settings.VIDEO_ZOOM_CLIENT_SECRET is not None:
|
||||
VIDEO_CHAT_PROVIDERS['zoom'] = {
|
||||
'name': "Zoom",
|
||||
'id': 3,
|
||||
}
|
||||
|
||||
if settings.BIG_BLUE_BUTTON_SECRET is not None and settings.BIG_BLUE_BUTTON_URL is not None:
|
||||
VIDEO_CHAT_PROVIDERS['big_blue_button'] = {
|
||||
'name': "Big Blue Button",
|
||||
'id': 4
|
||||
}
|
||||
|
||||
video_chat_provider = models.PositiveSmallIntegerField(default=VIDEO_CHAT_PROVIDERS['jitsi_meet']['id'])
|
||||
|
||||
default_code_block_language: Optional[str] = models.TextField(null=True, default=None)
|
||||
|
|
|
@ -88,6 +88,11 @@ EXCLUDE_PROPERTIES = {
|
|||
'200': ['deliver_at'],
|
||||
}
|
||||
},
|
||||
'/calls/bigbluebutton/create': {
|
||||
'get': {
|
||||
'200': ['url']
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# A list of endpoint-methods such that the endpoint
|
||||
|
|
|
@ -4495,6 +4495,27 @@ paths:
|
|||
"bot_email": "outgoing-bot@localhost"
|
||||
}
|
||||
|
||||
/calls/bigbluebutton/create:
|
||||
get:
|
||||
tags: ["streams"]
|
||||
operationId: create_big_blue_button_video_call
|
||||
description: |
|
||||
Create a video call url for a Big Blue Button video call.
|
||||
Requires Big Blue Button to be configured on the Zulip server.
|
||||
responses:
|
||||
'200':
|
||||
description: Success.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/JsonSuccess'
|
||||
example:
|
||||
{
|
||||
"msg": "",
|
||||
"result": "success",
|
||||
"url": "/calls/bbb/join?meeting_id=%22zulip-something%22&password=%22something%22&checksum=%22somechecksum%22"
|
||||
}
|
||||
|
||||
components:
|
||||
#######################
|
||||
# Security definitions
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
from unittest import mock
|
||||
|
||||
import responses
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
|
||||
|
@ -157,3 +160,57 @@ class TestVideoCall(ZulipTestCase):
|
|||
content_type="application/json",
|
||||
)
|
||||
self.assert_json_success(response)
|
||||
|
||||
def test_create_bigbluebutton_link(self) -> None:
|
||||
with mock.patch('zerver.views.video_calls.random.randint', return_value="1"), mock.patch(
|
||||
'zerver.views.video_calls.random.SystemRandom.choice', return_value="A"):
|
||||
response = self.client_get("/json/calls/bigbluebutton/create")
|
||||
self.assert_json_success(response)
|
||||
self.assertEqual(response.json()['url'],
|
||||
"/calls/bigbluebutton/join?meeting_id=%22zulip-1%22&password=%22AAAAAAAAAA%22"
|
||||
"&checksum=%22697939301a52d3a2f0b3c3338895c1a5ab528933%22")
|
||||
|
||||
@responses.activate
|
||||
def test_join_bigbluebutton_redirect(self) -> None:
|
||||
responses.add(responses.GET, "https://bbb.example.com/bigbluebutton/api/create?meetingID=zulip-1&moderatorPW=a&attendeePW=aa&checksum=check",
|
||||
"<response><returncode>SUCCESS</returncode><messageKey/></response>")
|
||||
response = self.client_get("/calls/bigbluebutton/join?meeting_id=%22zulip-1%22&password=%22a%22&checksum=%22check%22")
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(isinstance(response, HttpResponseRedirect), True)
|
||||
self.assertEqual(response.url, "https://bbb.example.com/bigbluebutton/api/join?meetingID=zulip-1&password=a"
|
||||
"&fullName=King%20Hamlet&checksum=7ddbb4e7e5aa57cb8c58db12003f3b5b040ff530")
|
||||
|
||||
@responses.activate
|
||||
def test_join_bigbluebutton_redirect_wrong_check(self) -> None:
|
||||
responses.add(responses.GET,
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=zulip-1&moderatorPW=a&attendeePW=aa&checksum=check",
|
||||
"<response><returncode>FAILED</returncode><messageKey>checksumError</messageKey>"
|
||||
"<message>You did not pass the checksum security check</message></response>")
|
||||
response = self.client_get("/calls/bigbluebutton/join?meeting_id=%22zulip-1%22&password=%22a%22&checksum=%22check%22")
|
||||
self.assert_json_error(response, "Error authenticating to the Big Blue Button server.")
|
||||
|
||||
@responses.activate
|
||||
def test_join_bigbluebutton_redirect_server_error(self) -> None:
|
||||
# Simulate bbb server error
|
||||
responses.add(responses.GET,
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=zulip-1&moderatorPW=a&attendeePW=aa&checksum=check", "", status=500)
|
||||
response = self.client_get(
|
||||
"/calls/bigbluebutton/join?meeting_id=%22zulip-1%22&password=%22a%22&checksum=%22check%22")
|
||||
self.assert_json_error(response, "Error connecting to the Big Blue Button server.")
|
||||
|
||||
@responses.activate
|
||||
def test_join_bigbluebutton_redirect_error_by_server(self) -> None:
|
||||
# Simulate bbb server error
|
||||
responses.add(responses.GET,
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=zulip-1&moderatorPW=a&attendeePW=aa&checksum=check",
|
||||
"<response><returncode>FAILURE</returncode><messageKey>otherFailure</messageKey></response>")
|
||||
response = self.client_get(
|
||||
"/calls/bigbluebutton/join?meeting_id=%22zulip-1%22&password=%22a%22&checksum=%22check%22")
|
||||
self.assert_json_error(response, "Big Blue Button server returned an unexpected error.")
|
||||
|
||||
def test_join_bigbluebutton_redirect_not_configured(self) -> None:
|
||||
with self.settings(BIG_BLUE_BUTTON_SECRET=None,
|
||||
BIG_BLUE_BUTTON_URL=None):
|
||||
response = self.client_get(
|
||||
"/calls/bigbluebutton/join?meeting_id=%22zulip-1%22&password=%22a%22&checksum=%22check%22")
|
||||
self.assert_json_error(response, "Big Blue Button is not configured.")
|
||||
|
|
|
@ -504,7 +504,7 @@ class RealmTest(ZulipTestCase):
|
|||
invite_to_stream_policy=10,
|
||||
email_address_visibility=10,
|
||||
message_retention_days=10,
|
||||
video_chat_provider=4,
|
||||
video_chat_provider=10,
|
||||
waiting_period_threshold=-10,
|
||||
digest_weekday=10,
|
||||
user_group_edit_policy=10,
|
||||
|
@ -539,7 +539,7 @@ class RealmTest(ZulipTestCase):
|
|||
self.assertEqual(get_realm('zulip').video_chat_provider, Realm.VIDEO_CHAT_PROVIDERS['jitsi_meet']['id'])
|
||||
self.login('iago')
|
||||
|
||||
invalid_video_chat_provider_value = 4
|
||||
invalid_video_chat_provider_value = 10
|
||||
req = {"video_chat_provider": ujson.dumps(invalid_video_chat_provider_value)}
|
||||
result = self.client_patch('/json/realm', req)
|
||||
self.assert_json_error(result,
|
||||
|
@ -556,6 +556,12 @@ class RealmTest(ZulipTestCase):
|
|||
self.assert_json_success(result)
|
||||
self.assertEqual(get_realm('zulip').video_chat_provider, Realm.VIDEO_CHAT_PROVIDERS['jitsi_meet']['id'])
|
||||
|
||||
req = {"video_chat_provider": ujson.dumps(Realm.VIDEO_CHAT_PROVIDERS['big_blue_button']['id'])}
|
||||
result = self.client_patch('/json/realm', req)
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(get_realm('zulip').video_chat_provider,
|
||||
Realm.VIDEO_CHAT_PROVIDERS['big_blue_button']['id'])
|
||||
|
||||
req = {"video_chat_provider": ujson.dumps(Realm.VIDEO_CHAT_PROVIDERS['zoom']['id'])}
|
||||
result = self.client_patch('/json/realm', req)
|
||||
self.assert_json_success(result)
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
from functools import partial
|
||||
from typing import Dict
|
||||
from urllib.parse import urljoin
|
||||
from urllib.parse import quote, urlencode, urljoin
|
||||
|
||||
import requests
|
||||
from defusedxml import ElementTree
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.middleware import csrf
|
||||
|
@ -20,8 +24,9 @@ from zerver.decorator import REQ, has_request_variables, zulip_login_required
|
|||
from zerver.lib.actions import do_set_zoom_token
|
||||
from zerver.lib.exceptions import ErrorCode, JsonableError
|
||||
from zerver.lib.pysa import mark_sanitized
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.response import json_error, json_success
|
||||
from zerver.lib.subdomains import get_subdomain
|
||||
from zerver.lib.url_encoding import add_query_arg_to_redirect_url, add_query_to_redirect_url
|
||||
from zerver.lib.validator import check_dict, check_string
|
||||
from zerver.models import UserProfile, get_realm
|
||||
|
||||
|
@ -135,7 +140,6 @@ def make_zoom_video_call(request: HttpRequest, user: UserProfile) -> HttpRespons
|
|||
|
||||
return json_success({"url": res.json()["join_url"]})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
@has_request_variables
|
||||
|
@ -155,3 +159,62 @@ def deauthorize_zoom_user(request: HttpRequest) -> HttpResponse:
|
|||
auth=(settings.VIDEO_ZOOM_CLIENT_ID, settings.VIDEO_ZOOM_CLIENT_SECRET),
|
||||
).raise_for_status()
|
||||
return json_success()
|
||||
|
||||
|
||||
def get_bigbluebutton_url(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
||||
# https://docs.bigbluebutton.org/dev/api.html#create for reference on the api calls
|
||||
# https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum
|
||||
id = "zulip-" + str(random.randint(100000000000, 999999999999))
|
||||
password = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(10))
|
||||
checksum = hashlib.sha1(("create" + "meetingID=" + id + "&moderatorPW="
|
||||
+ password + "&attendeePW=" + password + "a" + settings.BIG_BLUE_BUTTON_SECRET).encode()).hexdigest()
|
||||
url = add_query_to_redirect_url("/calls/bigbluebutton/join", urlencode({
|
||||
"meeting_id": "\"" + id + "\"",
|
||||
"password": "\"" + password + "\"",
|
||||
"checksum": "\"" + checksum + "\""
|
||||
}))
|
||||
return json_success({"url": url})
|
||||
|
||||
|
||||
# We use zulip_login_required here mainly to get access to the user's
|
||||
# full name from Zulip to prepopulate the user's name in the
|
||||
# BigBlueButton meeting. Since the meeting's details are encoded in
|
||||
# the link the user is clicking, there is no validation tying this
|
||||
# meeting to the Zulip organization it was created in.
|
||||
@zulip_login_required
|
||||
@never_cache
|
||||
@has_request_variables
|
||||
def join_bigbluebutton(request: HttpRequest, meeting_id: str = REQ(validator=check_string),
|
||||
password: str = REQ(validator=check_string),
|
||||
checksum: str = REQ(validator=check_string)) -> HttpResponse:
|
||||
if settings.BIG_BLUE_BUTTON_URL is None or settings.BIG_BLUE_BUTTON_SECRET is None:
|
||||
return json_error(_("Big Blue Button is not configured."))
|
||||
else:
|
||||
response = requests.get(
|
||||
add_query_to_redirect_url(settings.BIG_BLUE_BUTTON_URL + "api/create", urlencode({
|
||||
"meetingID": meeting_id,
|
||||
"moderatorPW": password,
|
||||
"attendeePW": password + "a",
|
||||
"checksum": checksum
|
||||
})))
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except Exception:
|
||||
return json_error(_("Error connecting to the Big Blue Button server."))
|
||||
|
||||
payload = ElementTree.fromstring(response.text)
|
||||
if payload.find("messageKey").text == "checksumError":
|
||||
return json_error(_("Error authenticating to the Big Blue Button server."))
|
||||
|
||||
if payload.find("returncode").text != "SUCCESS":
|
||||
return json_error(_("Big Blue Button server returned an unexpected error."))
|
||||
|
||||
join_params = urlencode({ # type: ignore[type-var] # MyPy has an AnyStr / Union[bytes, str] mismatch here.
|
||||
"meetingID": meeting_id,
|
||||
"password": password,
|
||||
"fullName": request.user.full_name
|
||||
}, quote_via=quote)
|
||||
|
||||
checksum = hashlib.sha1(("join" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()).hexdigest()
|
||||
redirect_url_base = add_query_to_redirect_url(settings.BIG_BLUE_BUTTON_URL + "api/join", join_params)
|
||||
return redirect(add_query_arg_to_redirect_url(redirect_url_base, "checksum=" + checksum))
|
||||
|
|
|
@ -458,6 +458,8 @@ ANDROID_GCM_API_KEY = get_secret("android_gcm_api_key")
|
|||
|
||||
DROPBOX_APP_KEY = get_secret("dropbox_app_key")
|
||||
|
||||
BIG_BLUE_BUTTON_SECRET = get_secret('big_blue_button_secret')
|
||||
|
||||
MAILCHIMP_API_KEY = get_secret("mailchimp_api_key")
|
||||
|
||||
# Twitter API credentials
|
||||
|
|
|
@ -116,6 +116,10 @@ MAX_FILE_UPLOAD_SIZE = 25
|
|||
# Jitsi Meet video call integration; set to None to disable integration.
|
||||
JITSI_SERVER_URL = 'https://meet.jit.si/'
|
||||
|
||||
# Allow setting BigBlueButton settings in zulip-secrets.conf in
|
||||
# development; this is useful since there are no public BigBlueButton servers.
|
||||
BIG_BLUE_BUTTON_URL = get_secret('big_blue_button_url', development_only=True)
|
||||
|
||||
# Max state storage per user
|
||||
# TODO: Add this to zproject/prod_settings_template.py once stateful bots are fully functional.
|
||||
USER_STATE_SIZE_LIMIT = 10000000
|
||||
|
|
|
@ -649,3 +649,7 @@ CAMO_URI = '/external_content/'
|
|||
# your own Jitsi Meet server, or if you'd like to disable the
|
||||
# integration, set JITSI_SERVER_URL = None.
|
||||
#JITSI_SERVER_URL = 'jitsi.example.com'
|
||||
|
||||
# Controls the Big Blue Button video call integration. You must also
|
||||
# set big_blue_button_secret in zulip-secrets.conf.
|
||||
# BIG_BLUE_BUTTON_URL = "https://bbb.example.com/bigbluebutton/"
|
||||
|
|
|
@ -185,6 +185,9 @@ APPLE_ID_TOKEN_GENERATION_KEY = get_from_file_if_exists("zerver/tests/fixtures/a
|
|||
VIDEO_ZOOM_CLIENT_ID = "client_id"
|
||||
VIDEO_ZOOM_CLIENT_SECRET = "client_secret"
|
||||
|
||||
BIG_BLUE_BUTTON_SECRET = "123"
|
||||
BIG_BLUE_BUTTON_URL = "https://bbb.example.com/bigbluebutton/"
|
||||
|
||||
# By default two factor authentication is disabled in tests.
|
||||
# Explicitly set this to True within tests that must have this on.
|
||||
TWO_FACTOR_AUTHENTICATION_ENABLED = False
|
||||
|
|
|
@ -405,6 +405,9 @@ v1_api_and_json_patterns = [
|
|||
# Used to generate a Zoom video call URL
|
||||
url(r'^calls/zoom/create$', rest_dispatch,
|
||||
{'POST': 'zerver.views.video_calls.make_zoom_video_call'}),
|
||||
# Used to generate a Big Blue Button video call URL
|
||||
url(r'^calls/bigbluebutton/create$', rest_dispatch,
|
||||
{'GET': 'zerver.views.video_calls.get_bigbluebutton_url'}),
|
||||
|
||||
# export/realm -> zerver.views.realm_export
|
||||
url(r'^export/realm$', rest_dispatch,
|
||||
|
@ -554,6 +557,9 @@ i18n_urls = [
|
|||
url(r'^calls/zoom/complete$', zerver.views.video_calls.complete_zoom_user),
|
||||
url(r'^calls/zoom/deauthorize$', zerver.views.video_calls.deauthorize_zoom_user),
|
||||
|
||||
# Used to join a Big Blue Button video call
|
||||
url(r'^calls/bigbluebutton/join$', zerver.views.video_calls.join_bigbluebutton),
|
||||
|
||||
# API and integrations documentation
|
||||
url(r'^integrations/doc-html/(?P<integration_name>[^/]*)$',
|
||||
zerver.views.documentation.integration_doc,
|
||||
|
|
Loading…
Reference in New Issue