0001-add-connector-for-BigBlueButton-66156.patch
passerelle/apps/bbb/migrations/0001_initial.py | ||
---|---|---|
1 |
# Generated by Django 2.2.28 on 2022-06-15 08:22 |
|
2 | ||
3 |
import uuid |
|
4 | ||
5 |
import django.db.models.deletion |
|
6 |
from django.db import migrations, models |
|
7 | ||
8 | ||
9 |
class Migration(migrations.Migration): |
|
10 | ||
11 |
initial = True |
|
12 | ||
13 |
dependencies = [ |
|
14 |
('base', '0029_auto_20210202_1627'), |
|
15 |
] |
|
16 | ||
17 |
operations = [ |
|
18 |
migrations.CreateModel( |
|
19 |
name='Resource', |
|
20 |
fields=[ |
|
21 |
( |
|
22 |
'id', |
|
23 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
24 |
), |
|
25 |
('title', models.CharField(max_length=50, verbose_name='Title')), |
|
26 |
('slug', models.SlugField(unique=True, verbose_name='Identifier')), |
|
27 |
('description', models.TextField(verbose_name='Description')), |
|
28 |
( |
|
29 |
'basic_auth_username', |
|
30 |
models.CharField( |
|
31 |
blank=True, max_length=128, verbose_name='Basic authentication username' |
|
32 |
), |
|
33 |
), |
|
34 |
( |
|
35 |
'basic_auth_password', |
|
36 |
models.CharField( |
|
37 |
blank=True, max_length=128, verbose_name='Basic authentication password' |
|
38 |
), |
|
39 |
), |
|
40 |
( |
|
41 |
'client_certificate', |
|
42 |
models.FileField( |
|
43 |
blank=True, null=True, upload_to='', verbose_name='TLS client certificate' |
|
44 |
), |
|
45 |
), |
|
46 |
( |
|
47 |
'trusted_certificate_authorities', |
|
48 |
models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs'), |
|
49 |
), |
|
50 |
( |
|
51 |
'verify_cert', |
|
52 |
models.BooleanField(blank=True, default=True, verbose_name='TLS verify certificates'), |
|
53 |
), |
|
54 |
( |
|
55 |
'http_proxy', |
|
56 |
models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy'), |
|
57 |
), |
|
58 |
( |
|
59 |
'bbb_url', |
|
60 |
models.URLField( |
|
61 |
help_text='Base URL of Big Blue Button (use "bbb-conf --secret" to get it)', |
|
62 |
max_length=400, |
|
63 |
verbose_name='BBB URL', |
|
64 |
), |
|
65 |
), |
|
66 |
( |
|
67 |
'shared_secret', |
|
68 |
models.CharField( |
|
69 |
help_text='Shared ecret (use "bbb-conf --secret" to get it)', max_length=128 |
|
70 |
), |
|
71 |
), |
|
72 |
( |
|
73 |
'users', |
|
74 |
models.ManyToManyField( |
|
75 |
blank=True, |
|
76 |
related_name='_resource_users_+', |
|
77 |
related_query_name='+', |
|
78 |
to='base.ApiUser', |
|
79 |
), |
|
80 |
), |
|
81 |
], |
|
82 |
options={ |
|
83 |
'verbose_name': 'Big Blue Button', |
|
84 |
}, |
|
85 |
), |
|
86 |
migrations.CreateModel( |
|
87 |
name='Meeting', |
|
88 |
fields=[ |
|
89 |
( |
|
90 |
'id', |
|
91 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
92 |
), |
|
93 |
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), |
|
94 |
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')), |
|
95 |
('guid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID')), |
|
96 |
('name', models.TextField(verbose_name='Name')), |
|
97 |
('idempotent_id', models.TextField(unique=True, verbose_name='Idempotent ID')), |
|
98 |
('running', models.BooleanField(default=False, verbose_name='Is running?')), |
|
99 |
('last_time_running', models.DateTimeField(null=True, verbose_name='Last time running')), |
|
100 |
('create_parameters', models.TextField(null=True, verbose_name='Create parameters')), |
|
101 |
( |
|
102 |
'resource', |
|
103 |
models.ForeignKey( |
|
104 |
on_delete=django.db.models.deletion.CASCADE, |
|
105 |
related_name='meetings', |
|
106 |
to='bbb.Resource', |
|
107 |
verbose_name='Resource', |
|
108 |
), |
|
109 |
), |
|
110 |
], |
|
111 |
), |
|
112 |
] |
passerelle/apps/bbb/models.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import hashlib |
|
18 |
import json |
|
19 |
import time |
|
20 |
import uuid |
|
21 | ||
22 |
from django.core.cache import cache |
|
23 |
from django.core.exceptions import PermissionDenied |
|
24 |
from django.db import models, transaction |
|
25 |
from django.http import HttpResponseRedirect, JsonResponse |
|
26 |
from django.shortcuts import get_object_or_404 |
|
27 |
from django.urls import reverse |
|
28 |
from django.utils.functional import cached_property |
|
29 |
from django.utils.timezone import now |
|
30 |
from django.utils.translation import gettext_lazy as _ |
|
31 | ||
32 |
from passerelle.base.models import BaseResource, HTTPResource |
|
33 |
from passerelle.utils.api import endpoint |
|
34 |
from passerelle.utils.jsonresponse import APIError |
|
35 | ||
36 |
from . import utils |
|
37 | ||
38 | ||
39 |
def complete_url(request, data): |
|
40 |
data = dict(data) |
|
41 |
for key in data: |
|
42 |
if key == 'url' or key.endswith('_url') and data[key]: |
|
43 |
data[key] = request.build_absolute_uri(data[key]) |
|
44 |
return data |
|
45 | ||
46 | ||
47 |
class Resource(BaseResource, HTTPResource): |
|
48 |
bbb_url = models.URLField( |
|
49 |
max_length=400, |
|
50 |
verbose_name=_('BBB URL'), |
|
51 |
help_text=_('Base URL of Big Blue Button (use "bbb-conf --secret" to get it)'), |
|
52 |
) |
|
53 |
shared_secret = models.CharField( |
|
54 |
max_length=128, help_text=_('Shared ecret (use "bbb-conf --secret" to get it)') |
|
55 |
) |
|
56 |
category = _('Business Process Connectors') |
|
57 | ||
58 |
class Meta: |
|
59 |
verbose_name = _('Big Blue Button') |
|
60 | ||
61 |
@cached_property |
|
62 |
def bbb(self): |
|
63 |
return utils.BBB(url=self.bbb_url, shared_secret=self.shared_secret, session=self.requests) |
|
64 | ||
65 |
def check_status(self): |
|
66 |
try: |
|
67 |
self.bbb.get_meetings() |
|
68 |
except utils.BBB.BBBError as e: |
|
69 |
raise APIError(e) |
|
70 | ||
71 |
@endpoint( |
|
72 |
methods=['post'], |
|
73 |
name='meeting', |
|
74 |
perm='can_access', |
|
75 |
description_post=_('Create a meeting'), |
|
76 |
post={ |
|
77 |
'request_body': { |
|
78 |
'schema': { |
|
79 |
'application/json': { |
|
80 |
'type': 'object', |
|
81 |
'properties': { |
|
82 |
'name': { |
|
83 |
'type': 'string', |
|
84 |
}, |
|
85 |
'idempotent_id': { |
|
86 |
'type': 'string', |
|
87 |
}, |
|
88 |
'create_parameters': { |
|
89 |
'type': 'object', |
|
90 |
'properties': { |
|
91 |
'logoutURL': {'type': 'string'}, |
|
92 |
}, |
|
93 |
'additionalProperties': True, |
|
94 |
}, |
|
95 |
}, |
|
96 |
'required': ['name', 'idempotent_id'], |
|
97 |
'unflatten': True, |
|
98 |
} |
|
99 |
} |
|
100 |
} |
|
101 |
}, |
|
102 |
parameters={ |
|
103 |
'update': { |
|
104 |
'description': _('Update existing meeting'), |
|
105 |
'example_value': '1', |
|
106 |
}, |
|
107 |
}, |
|
108 |
) |
|
109 |
@transaction.atomic |
|
110 |
def meetings_endpoint(self, request, post_data, update=0): |
|
111 |
create_parameters = ( |
|
112 |
json.dumps(post_data['create_parameters']) if post_data.get('create_parameters') else None |
|
113 |
) |
|
114 |
if update == '1': |
|
115 |
meeting, dummy = self.meetings.update_or_create( |
|
116 |
idempotent_id=post_data['idempotent_id'], |
|
117 |
defaults={ |
|
118 |
'name': post_data['name'], |
|
119 |
'create_parameters': create_parameters, |
|
120 |
}, |
|
121 |
) |
|
122 |
else: |
|
123 |
meeting, created = self.meetings.get_or_create( |
|
124 |
idempotent_id=post_data['idempotent_id'], |
|
125 |
defaults={ |
|
126 |
'name': post_data['name'], |
|
127 |
'create_parameters': create_parameters, |
|
128 |
}, |
|
129 |
) |
|
130 |
if not created: |
|
131 |
if meeting.name != post_data['name'] or meeting.create_kwargs != post_data.get( |
|
132 |
'create_parameters', {} |
|
133 |
): |
|
134 |
raise APIError('meeting already exists with different name of create_parameters') |
|
135 |
try: |
|
136 |
meeting.create() |
|
137 |
except utils.BBB.BBBError as e: |
|
138 |
raise APIError(e) |
|
139 |
return {'data': complete_url(request, meeting.to_json())} |
|
140 | ||
141 |
@endpoint( |
|
142 |
methods=['get'], |
|
143 |
name='meeting', |
|
144 |
perm='can_access', |
|
145 |
pattern=r'^(?P<guid>[0-9a-f]{32})/?$', |
|
146 |
example_pattern='{guid}/', |
|
147 |
description_post=_('Get a meeting'), |
|
148 |
parameters={ |
|
149 |
'guid': { |
|
150 |
'description': _('Meeting guid'), |
|
151 |
'example_value': '7edb43abf2004f55a8a526ac4b1403e4', |
|
152 |
}, |
|
153 |
}, |
|
154 |
) |
|
155 |
def meeting_endpoint(self, request, guid): |
|
156 |
meeting = get_object_or_404(Meeting.objects.select_for_update(), guid=guid) |
|
157 |
return {'data': complete_url(request, meeting.to_json())} |
|
158 | ||
159 |
@endpoint( |
|
160 |
methods=['get'], |
|
161 |
name='meeting', |
|
162 |
pattern=r'^(?P<guid>[0-9a-f]{32})/is-running/?$', |
|
163 |
example_pattern='{guid}/is-running/', |
|
164 |
description_post=_('Report if meeting is running'), |
|
165 |
parameters={ |
|
166 |
'guid': { |
|
167 |
'description': _('Meeting guid'), |
|
168 |
'example_value': '7edb43abf2004f55a8a526ac4b1403e4', |
|
169 |
}, |
|
170 |
}, |
|
171 |
) |
|
172 |
@transaction.atomic |
|
173 |
def is_running(self, request, guid): |
|
174 |
meeting = get_object_or_404(Meeting.objects.select_for_update(), guid=guid) |
|
175 |
if (now() - meeting.updated).total_seconds() > 5: |
|
176 |
meeting.update_is_running() |
|
177 |
response = JsonResponse({'err': 0, 'data': meeting.running}) |
|
178 |
response['Access-Control-Allow-Origin'] = '*' |
|
179 |
return response |
|
180 | ||
181 |
@endpoint( |
|
182 |
methods=['get'], |
|
183 |
name='meeting', |
|
184 |
pattern=r'^(?P<guid>[0-9a-f]{32})/join/agent/(?P<key>[^/]*)/?$', |
|
185 |
example_pattern='{guid}/join/agent/', |
|
186 |
description_post=_('Get a meeting'), |
|
187 |
parameters={ |
|
188 |
'guid': { |
|
189 |
'description': _('Meeting guid'), |
|
190 |
'example_value': '7edb43abf2004f55a8a526ac4b1403e4', |
|
191 |
}, |
|
192 |
'full_name': { |
|
193 |
'description': _('Agent full name'), |
|
194 |
'example_value': 'John Doe', |
|
195 |
}, |
|
196 |
'key': { |
|
197 |
'description': _('Secret key'), |
|
198 |
'example_value': '1234', |
|
199 |
}, |
|
200 |
}, |
|
201 |
) |
|
202 |
def join_agent(self, request, guid, full_name, key): |
|
203 |
meeting = get_object_or_404(Meeting, guid=guid) |
|
204 |
if key != meeting.agent_key: |
|
205 |
raise PermissionDenied |
|
206 |
url = meeting.create().join_url(full_name, self.bbb.ROLE_MODERATOR) |
|
207 |
return HttpResponseRedirect(url) |
|
208 | ||
209 |
@endpoint( |
|
210 |
methods=['get'], |
|
211 |
name='meeting', |
|
212 |
pattern=r'^(?P<guid>[0-9a-f]{32})/join/user/(?P<key>[^/]*)/?$', |
|
213 |
example_pattern='{guid}/join/user/', |
|
214 |
description_post=_('Get a meeting'), |
|
215 |
parameters={ |
|
216 |
'guid': { |
|
217 |
'description': _('Meeting guid'), |
|
218 |
'example_value': '7edb43abf2004f55a8a526ac4b1403e4', |
|
219 |
}, |
|
220 |
'full_name': { |
|
221 |
'description': _('User full name'), |
|
222 |
'example_value': 'John Doe', |
|
223 |
}, |
|
224 |
'key': { |
|
225 |
'description': _('Secret key'), |
|
226 |
'example_value': '1234', |
|
227 |
}, |
|
228 |
}, |
|
229 |
) |
|
230 |
def join_user(self, request, guid, full_name, key): |
|
231 |
meeting = get_object_or_404(Meeting, guid=guid) |
|
232 |
if key != meeting.user_key: |
|
233 |
raise PermissionDenied |
|
234 |
url = meeting.create().join_url(full_name, self.bbb.ROLE_VIEWER) |
|
235 |
return HttpResponseRedirect(url) |
|
236 | ||
237 |
def _make_endpoint_url(self, endpoint, rest): |
|
238 |
return reverse( |
|
239 |
'generic-endpoint', |
|
240 |
kwargs={'connector': 'bbb', 'endpoint': endpoint, 'slug': self.slug, 'rest': rest}, |
|
241 |
) |
|
242 | ||
243 | ||
244 |
class Meeting(models.Model): |
|
245 |
created = models.DateTimeField(verbose_name=_('Created'), auto_now_add=True) |
|
246 |
updated = models.DateTimeField(verbose_name=_('Updated'), auto_now=True) |
|
247 |
resource = models.ForeignKey( |
|
248 |
Resource, verbose_name=_('Resource'), on_delete=models.CASCADE, related_name='meetings' |
|
249 |
) |
|
250 |
guid = models.UUIDField(verbose_name=_('UUID'), unique=True, default=uuid.uuid4) |
|
251 |
name = models.TextField(verbose_name=_('Name')) |
|
252 |
idempotent_id = models.TextField(verbose_name=_('Idempotent ID'), unique=True) |
|
253 |
running = models.BooleanField(verbose_name=_('Is running?'), default=False) |
|
254 |
last_time_running = models.DateTimeField(verbose_name=_('Last time running'), null=True) |
|
255 |
create_parameters = models.TextField('Create parameters', null=True) |
|
256 | ||
257 |
def _make_key(self, _for): |
|
258 |
return hashlib.sha1((self.resource.shared_secret + _for + self.meeting_id).encode()).hexdigest()[:6] |
|
259 | ||
260 |
@property |
|
261 |
def user_key(self): |
|
262 |
return self._make_key('user') |
|
263 | ||
264 |
@property |
|
265 |
def join_user_url(self): |
|
266 |
return self.resource._make_endpoint_url( |
|
267 |
endpoint='meeting', rest=f'{self.guid.hex}/join/user/{self.user_key}/' |
|
268 |
) |
|
269 | ||
270 |
@property |
|
271 |
def agent_key(self): |
|
272 |
return self._make_key('agent') |
|
273 | ||
274 |
@property |
|
275 |
def join_agent_url(self): |
|
276 |
return self.resource._make_endpoint_url( |
|
277 |
endpoint='meeting', rest=f'{self.guid.hex}/join/agent/{self.agent_key}/' |
|
278 |
) |
|
279 | ||
280 |
@property |
|
281 |
def url(self): |
|
282 |
return self.resource._make_endpoint_url(endpoint='meeting', rest=f'{self.guid.hex}/') |
|
283 | ||
284 |
@property |
|
285 |
def is_running_url(self): |
|
286 |
return self.resource._make_endpoint_url(endpoint='meeting', rest=f'{self.guid.hex}/') |
|
287 | ||
288 |
def to_json(self): |
|
289 |
return { |
|
290 |
'guid': self.guid.hex, |
|
291 |
'created': self.created, |
|
292 |
'updated': self.updated, |
|
293 |
'name': self.name, |
|
294 |
'idempotent_id': self.idempotent_id, |
|
295 |
'running': self.running, |
|
296 |
'last_time_running': self.last_time_running, |
|
297 |
'url': self.url, |
|
298 |
'join_user_url': self.join_user_url, |
|
299 |
'join_agent_url': self.join_agent_url, |
|
300 |
'is_running_url': self.is_running_url, |
|
301 |
'bbb_meeting_info': self.meeting_info(), |
|
302 |
} |
|
303 | ||
304 |
@property |
|
305 |
def meeting_id(self): |
|
306 |
return self.guid.hex |
|
307 | ||
308 |
def update_is_running(self): |
|
309 |
try: |
|
310 |
running = self.resource.bbb.is_meeting_running(self.meeting_id)['running'] == 'true' |
|
311 |
except self.resource.bbb.BBBError: |
|
312 |
return |
|
313 |
if self.running != running: |
|
314 |
self.running = running |
|
315 |
if running: |
|
316 |
self.last_time_running = now() |
|
317 |
self.save(update_fields=['updated', 'last_time_running', 'running']) |
|
318 |
else: |
|
319 |
self.save(update_fields=['updated', 'running']) |
|
320 | ||
321 |
@property |
|
322 |
def create_kwargs(self): |
|
323 |
return json.loads(self.create_parameters) if self.create_parameters else {} |
|
324 | ||
325 |
def create(self): |
|
326 |
return self.resource.bbb.meetings.create( |
|
327 |
name=self.name, meeting_id=self.meeting_id, **self.create_kwargs |
|
328 |
) |
|
329 | ||
330 |
@property |
|
331 |
def cache_key(self): |
|
332 |
return f'bbb_{self.resource.slug}_{self.guid.hex}' |
|
333 | ||
334 |
def meeting_info(self): |
|
335 |
start = time.time() |
|
336 |
try: |
|
337 |
data, timestamp = cache.get(self.cache_key) |
|
338 |
except TypeError: |
|
339 |
data = None |
|
340 |
if data is None or (start - timestamp) > 30: |
|
341 |
try: |
|
342 |
data = self.resource.bbb.meetings.get(meeting_id=self.meeting_id).attributes |
|
343 |
except utils.BBB.BBBError: |
|
344 |
data = data or {} |
|
345 |
cache.set(self.cache_key, (data, start)) |
|
346 |
return data |
passerelle/apps/bbb/templates/bbb/resource_detail.html | ||
---|---|---|
1 |
{% extends "passerelle/manage/service_view.html" %} |
|
2 |
{% load i18n passerelle %} |
passerelle/apps/bbb/utils.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2019 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import hashlib |
|
18 |
import re |
|
19 |
import xml.etree.ElementTree as ET |
|
20 | ||
21 |
import requests |
|
22 |
from django.utils.http import urlencode |
|
23 | ||
24 | ||
25 |
class BBB: |
|
26 |
CALL_CREATE = 'create' |
|
27 |
CALL_END = 'end' |
|
28 |
CALL_GET_MEETING_INFO = 'getMeetingInfo' |
|
29 |
CALL_GET_MEETINGS = 'getMeetings' |
|
30 |
CALL_IS_MEETING_RUNNING = 'isMeetingRunning' |
|
31 |
CALL_JOIN = 'join' |
|
32 | ||
33 |
PARAM_ATTENDEE_PW = 'attendeePW' |
|
34 |
PARAM_CREATE_TIME = 'createTime' |
|
35 |
PARAM_FULL_NAME = 'fullName' |
|
36 |
PARAM_MEETING_ID = 'meetingID' |
|
37 |
PARAM_MODERATOR_PW = 'moderatorPW' |
|
38 |
PARAM_NAME = 'name' |
|
39 |
PARAM_ROLE = 'role' |
|
40 | ||
41 |
TAG_RETURN_CODE = 'returncode' |
|
42 |
TAG_MESSAGE_KEY = 'messageKey' |
|
43 |
TAG_MESSAGE = 'message' |
|
44 | ||
45 |
RETURN_CODE_SUCCESS = 'SUCCESS' |
|
46 |
RETURN_CODE_FAILED = 'FAILED' |
|
47 |
RETURN_CODES = [RETURN_CODE_SUCCESS, RETURN_CODE_FAILED] |
|
48 | ||
49 |
ROLE_MODERATOR = 'MODERATOR' |
|
50 |
ROLE_VIEWER = 'VIEWER' |
|
51 |
ROLES = [ROLE_MODERATOR, ROLE_VIEWER] |
|
52 | ||
53 |
MESSAGE_KEY_ID_NOT_UNIQUE = 'idNotUnique' |
|
54 |
MESSAGE_KEY_NOT_FOUND = 'notFound' |
|
55 |
MESSAGE_KEY_CHECKSUM_ERROR = 'checksumError' |
|
56 |
MESSAGE_KEY_SENT_END_MEETING_REQUEST = 'sentEndMeetingRequest' |
|
57 | ||
58 |
class BBBError(Exception): |
|
59 |
pass |
|
60 | ||
61 |
class FailedError(BBBError): |
|
62 |
def __init__(self, message, message_key=None): |
|
63 |
self.message_key = message_key |
|
64 |
super().__init__(message) |
|
65 | ||
66 |
def __repr__(self): |
|
67 |
return f'<{self.__class__.__name__} "{self}" message_key={self.message_key}>' |
|
68 | ||
69 |
class NotFoundError(FailedError): |
|
70 |
pass |
|
71 | ||
72 |
class IdNotUniqueError(FailedError): |
|
73 |
pass |
|
74 | ||
75 |
class ChecksumError(FailedError): |
|
76 |
pass |
|
77 | ||
78 |
MESSAGE_KEY_TO_CLASS = { |
|
79 |
MESSAGE_KEY_ID_NOT_UNIQUE: IdNotUniqueError, |
|
80 |
MESSAGE_KEY_NOT_FOUND: NotFoundError, |
|
81 |
MESSAGE_KEY_CHECKSUM_ERROR: ChecksumError, |
|
82 |
} |
|
83 | ||
84 |
@staticmethod |
|
85 |
def camel_to_snake(string): |
|
86 |
string = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', string) |
|
87 |
string = re.sub('(.)([0-9]+)', r'\1_\2', string) |
|
88 |
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', string).lower() |
|
89 | ||
90 |
def __init__(self, *, url, shared_secret, session=None): |
|
91 |
if not url.endswith('/'): |
|
92 |
raise self.BBBError(f'expected url ending with a slash, got {url!r}') |
|
93 |
self.url = url |
|
94 |
self.shared_secret = shared_secret |
|
95 |
self.http_session = session or requests.Session() |
|
96 |
self.meetings = self.Meetings(bbb=self) |
|
97 | ||
98 |
def _build_query(self, name, parameters): |
|
99 |
"""Method to create valid API query |
|
100 |
to call on BigBlueButton. Because each query |
|
101 |
should have a encrypted checksum based on request Data. |
|
102 |
""" |
|
103 |
query = urlencode(parameters) |
|
104 |
prepared = f'{name}{query}{self.shared_secret}' |
|
105 |
checksum = hashlib.sha1(prepared.encode('utf-8')).hexdigest() |
|
106 |
sep = '&' if query else '' |
|
107 |
result = f'{query}{sep}checksum={checksum}' |
|
108 |
return result |
|
109 | ||
110 |
def _http_get(self, url): |
|
111 |
try: |
|
112 |
raw_response = self.http_session.get(url) |
|
113 |
raw_response.raise_for_status() |
|
114 |
except requests.RequestException as e: |
|
115 |
raise self.BBBError('transport error', e) |
|
116 |
return raw_response |
|
117 | ||
118 |
@classmethod |
|
119 |
def _parse_xml_doc(cls, root, data): |
|
120 |
for child in root: |
|
121 |
if len(child) == 0: |
|
122 |
data[child.tag.split('}')[-1]] = child.text |
|
123 |
elif max(len(sub) for sub in child) == 0: |
|
124 |
# subobject case |
|
125 |
data[child.tag] = cls._parse_xml_doc(child, {}) |
|
126 |
else: |
|
127 |
# array case |
|
128 |
data[child.tag] = [cls._parse_xml_doc(sub, {}) for sub in child] |
|
129 |
return data |
|
130 | ||
131 |
@classmethod |
|
132 |
def _parse_response(cls, response): |
|
133 |
try: |
|
134 |
root = ET.fromstring(response.content) |
|
135 |
if root.tag != 'response': |
|
136 |
raise ValueError('root tag is not response') |
|
137 |
data = {} |
|
138 |
cls._parse_xml_doc(root, data) |
|
139 |
if cls.TAG_RETURN_CODE not in data: |
|
140 |
raise ValueError(f'expected a return code, got {data!r}') |
|
141 |
return_code = data.get(cls.TAG_RETURN_CODE) |
|
142 |
if return_code not in cls.RETURN_CODES: |
|
143 |
raise ValueError(f'expected a return code, got {data!r}') |
|
144 |
data.pop(cls.TAG_RETURN_CODE) |
|
145 |
return return_code, data |
|
146 |
except Exception as e: |
|
147 |
raise cls.BBBError('invalid response', e) |
|
148 | ||
149 |
def _make_url(self, name, parameters): |
|
150 |
query = self._build_query(name, parameters) |
|
151 |
return f'{self.url}api/{name}?{query}' |
|
152 | ||
153 |
def _api_call(self, name, parameters): |
|
154 |
url = self._make_url(name, parameters) |
|
155 |
raw_response = self._http_get(url) |
|
156 |
return_code, data = self._parse_response(raw_response) |
|
157 |
if return_code == self.RETURN_CODE_FAILED: |
|
158 |
exception_class = self.MESSAGE_KEY_TO_CLASS.get(data.get(self.TAG_MESSAGE_KEY), self.FailedError) |
|
159 |
raise exception_class( |
|
160 |
message=data.get(self.TAG_MESSAGE, 'NO_MESSAGE_KEY'), |
|
161 |
message_key=data.get(self.TAG_MESSAGE_KEY), |
|
162 |
) |
|
163 |
return data |
|
164 | ||
165 |
def create_meeting(self, name, meeting_id, attendee_pw=None, moderator_pw=None, **kwargs): |
|
166 |
parameters = { |
|
167 |
self.PARAM_NAME: name, |
|
168 |
self.PARAM_MEETING_ID: meeting_id, |
|
169 |
self.PARAM_ATTENDEE_PW: ( |
|
170 |
attendee_pw |
|
171 |
or hashlib.sha1((self.shared_secret + meeting_id + 'attendee').encode()).hexdigest() |
|
172 |
), |
|
173 |
self.PARAM_MODERATOR_PW: ( |
|
174 |
moderator_pw |
|
175 |
or hashlib.sha1((self.shared_secret + meeting_id + 'moderator').encode()).hexdigest() |
|
176 |
), |
|
177 |
} |
|
178 |
return self._api_call(self.CALL_CREATE, dict(parameters, **kwargs)) |
|
179 | ||
180 |
def get_meeting_info(self, meeting_id): |
|
181 |
parameters = { |
|
182 |
self.PARAM_MEETING_ID: meeting_id, |
|
183 |
} |
|
184 |
return self._api_call(self.CALL_GET_MEETING_INFO, parameters) |
|
185 | ||
186 |
def is_meeting_running(self, meeting_id): |
|
187 |
parameters = { |
|
188 |
self.PARAM_MEETING_ID: meeting_id, |
|
189 |
} |
|
190 |
try: |
|
191 |
return self._api_call(self.CALL_IS_MEETING_RUNNING, parameters) |
|
192 |
except self.NotFoundError: |
|
193 |
return False |
|
194 | ||
195 |
def end_meeting(self, meeting_id): |
|
196 |
parameters = { |
|
197 |
self.PARAM_MEETING_ID: meeting_id, |
|
198 |
} |
|
199 |
try: |
|
200 |
response = self._api_call(self.CALL_END, parameters) |
|
201 |
except self.NotFoundError: |
|
202 |
return False |
|
203 |
if response[self.TAG_MESSAGE_KEY] == self.MESSAGE_KEY_SENT_END_MEETING_REQUEST: |
|
204 |
return True |
|
205 |
raise self.BBBError(f'expected notFound or sentEndMeetingRequest, got {response!r}') |
|
206 | ||
207 |
def get_meetings(self): |
|
208 |
return self._api_call(self.CALL_GET_MEETINGS, {}) |
|
209 | ||
210 |
def make_join_url(self, full_name, meeting_id, role, create_time=None, **kwargs): |
|
211 |
if role not in self.ROLES: |
|
212 |
raise self.BBBError(f'expected a role value, got {role!r}') |
|
213 |
parameters = { |
|
214 |
self.PARAM_FULL_NAME: full_name, |
|
215 |
self.PARAM_MEETING_ID: meeting_id, |
|
216 |
self.PARAM_ROLE: role, |
|
217 |
} |
|
218 |
if create_time is not None: |
|
219 |
parameters[self.PARAM_CREATE_TIME] = create_time |
|
220 |
return self._make_url(self.CALL_JOIN, parameters) |
|
221 | ||
222 |
class Meeting: |
|
223 |
is_running = False |
|
224 |
meeting_name = None |
|
225 |
meeting_id = None |
|
226 | ||
227 |
def __init__(self, bbb: 'BBB', **kwargs): |
|
228 |
self.bbb = bbb |
|
229 |
# came to snake case |
|
230 |
parameters = {BBB.camel_to_snake(key): value for key, value in kwargs.items()} |
|
231 |
self.message = {k: v for k, v in parameters.items() if k.startswith('message')} |
|
232 |
self.meeting_id = parameters.pop('meeting_id') |
|
233 |
self.meeting_name = parameters.pop('meeting_name') |
|
234 |
self.attributes = {k: v for k, v in parameters.items() if not k.startswith('message')} |
|
235 | ||
236 |
def update(self): |
|
237 |
data = self.bbb.get_meeting_info(self.meeting_id) |
|
238 |
parameters = { |
|
239 |
BBB.camel_to_snake(key): value |
|
240 |
for key, value in data.items() |
|
241 |
if key not in ['meetingID', 'meetingName'] |
|
242 |
} |
|
243 |
self.message = {k: v for k, v in parameters.items() if k.startswith('message')} |
|
244 |
self.attributes = {k: v for k, v in parameters.items() if not k.startswith('message')} |
|
245 | ||
246 |
def update_is_running(self): |
|
247 |
self.is_running = self.bbb.is_meeting_running(self.meeting_id)['running'] == 'true' |
|
248 |
return self.is_running |
|
249 | ||
250 |
def end(self): |
|
251 |
try: |
|
252 |
return self.bbb.end_meeting(self.meeting_id) |
|
253 |
finally: |
|
254 |
self.is_running = False |
|
255 | ||
256 |
def join_url(self, full_name, role): |
|
257 |
return self.bbb.make_join_url( |
|
258 |
full_name=full_name, |
|
259 |
role=role, |
|
260 |
meeting_id=self.meeting_id, |
|
261 |
create_time=self.attributes['create_time'], |
|
262 |
) |
|
263 | ||
264 |
def to_dict(self): |
|
265 |
return {k: v for k, v in self.__dict__.items() if k not in ['bbb']} |
|
266 | ||
267 |
def __repr__(self): |
|
268 |
return f'<Meeting name={self.meeting_name} id={self.meeting_id}>' |
|
269 | ||
270 |
class Meetings: |
|
271 |
def __init__(self, bbb: 'BBB'): |
|
272 |
self.bbb = bbb |
|
273 | ||
274 |
def create(self, name, meeting_id, **kwargs): |
|
275 |
return BBB.Meeting( |
|
276 |
self.bbb, |
|
277 |
meeting_name=name, |
|
278 |
**self.bbb.create_meeting(name=name, meeting_id=meeting_id, **kwargs), |
|
279 |
) |
|
280 | ||
281 |
def get(self, meeting_id): |
|
282 |
return BBB.Meeting(self.bbb, **self.bbb.get_meeting_info(meeting_id=meeting_id)) |
|
283 | ||
284 |
def all(self): |
|
285 |
response = self.bbb.get_meetings() |
|
286 |
for response_meeting in response.get('meetings', []): |
|
287 |
yield BBB.Meeting(self.bbb, **response_meeting) |
passerelle/settings.py | ||
---|---|---|
132 | 132 |
'passerelle.apps.atal', |
133 | 133 |
'passerelle.apps.atos_genesys', |
134 | 134 |
'passerelle.apps.base_adresse', |
135 |
'passerelle.apps.bbb', |
|
135 | 136 |
'passerelle.apps.bdp', |
136 | 137 |
'passerelle.apps.cartads_cs', |
137 | 138 |
'passerelle.apps.choosit', |
tests/test_bbb.py | ||
---|---|---|
1 |
# Copyright (C) 2021 Entr'ouvert |
|
2 |
# |
|
3 |
# This program is free software: you can redistribute it and/or modify it |
|
4 |
# under the terms of the GNU Affero General Public License as published |
|
5 |
# by the Free Software Foundation, either version 3 of the License, or |
|
6 |
# (at your option) any later version. |
|
7 |
# |
|
8 |
# This program is distributed in the hope that it will be useful, |
|
9 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 |
# GNU Affero General Public License for more details. |
|
12 |
# |
|
13 |
# You should have received a copy of the GNU Affero General Public License |
|
14 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
15 | ||
16 |
import datetime |
|
17 |
import json |
|
18 |
import uuid |
|
19 | ||
20 |
import mock |
|
21 |
import pytest |
|
22 | ||
23 |
import passerelle.apps.bbb.utils as bbb_utils |
|
24 | ||
25 |
from . import utils |
|
26 | ||
27 |
BBB_URL = 'https://example.com/bigbluebutton/' |
|
28 |
SHARED_SECRET = 'ABCD' |
|
29 |
SLUG = 'test' |
|
30 |
MEETING_NAME = 'RDV' |
|
31 |
MEETING_ID = 'a' * 32 |
|
32 |
UUID = uuid.UUID(MEETING_ID) |
|
33 |
IDEMPOTENT_ID = '10-1999' |
|
34 |
LOGOUT_URL = 'https://portal/' |
|
35 | ||
36 | ||
37 |
@pytest.fixture |
|
38 |
def connector(db): |
|
39 |
from passerelle.apps.bbb.models import Resource |
|
40 | ||
41 |
return utils.setup_access_rights( |
|
42 |
Resource.objects.create( |
|
43 |
slug=SLUG, |
|
44 |
bbb_url=BBB_URL, |
|
45 |
shared_secret=SHARED_SECRET, |
|
46 |
) |
|
47 |
) |
|
48 | ||
49 | ||
50 |
class TestManage: |
|
51 |
pytestmark = pytest.mark.django_db |
|
52 | ||
53 |
@pytest.fixture |
|
54 |
def app(self, app, admin_user): |
|
55 |
from .test_manager import login |
|
56 | ||
57 |
login(app) |
|
58 |
return app |
|
59 | ||
60 |
def test_homepage(self, app, connector): |
|
61 |
app.get(f'/bbb/{SLUG}/') |
|
62 | ||
63 | ||
64 |
class TestBBB: |
|
65 |
@pytest.fixture |
|
66 |
def bbb(self): |
|
67 |
return bbb_utils.BBB(url=BBB_URL, shared_secret=SHARED_SECRET) |
|
68 | ||
69 |
@pytest.fixture |
|
70 |
def mock(self): |
|
71 |
with mock.patch('requests.Session.send') as requests_send: |
|
72 |
requests_send.return_value = mock.Mock() |
|
73 |
yield requests_send |
|
74 | ||
75 |
def test_create_failure(self, bbb, mock): |
|
76 |
mock.return_value.status_code = 200 |
|
77 |
mock.return_value.content = '<response/>' |
|
78 | ||
79 |
with pytest.raises(bbb.BBBError): |
|
80 |
bbb.create_meeting(name=MEETING_NAME, meeting_id=MEETING_ID) |
|
81 | ||
82 |
class TestCreate: |
|
83 |
@pytest.fixture |
|
84 |
def mock(self, mock): |
|
85 |
mock.return_value.content = '''<response> |
|
86 |
<returncode>SUCCESS</returncode> |
|
87 |
<message>ok</message> |
|
88 |
<meetingID>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</meetingID> |
|
89 |
</response>''' |
|
90 |
return mock |
|
91 | ||
92 |
def test_create(self, bbb, mock): |
|
93 |
result = bbb.create_meeting(name=MEETING_NAME, meeting_id=MEETING_ID) |
|
94 |
prepared_request = mock.call_args[0][0] |
|
95 |
assert prepared_request.url == ( |
|
96 |
'https://example.com/bigbluebutton/api/create?' |
|
97 |
'name=RDV&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&attendeePW=6256459f2baead794d599a1ad21a07fd4bc5743a' |
|
98 |
'&moderatorPW=2ebb4732fcbdd5a9f0751c3ffa7a5f51f755f980&checksum=4ac7d65fe6beb6f86fa5a13d6d8e0d1fadee0b50' |
|
99 |
) |
|
100 |
assert result == {'meetingID': MEETING_ID, 'message': 'ok'} |
|
101 | ||
102 |
def test_meetings_create(self, bbb, mock): |
|
103 |
meeting = bbb.meetings.create(name=MEETING_NAME, meeting_id=MEETING_ID) |
|
104 |
prepared_request = mock.call_args[0][0] |
|
105 |
assert prepared_request.url == ( |
|
106 |
'https://example.com/bigbluebutton/api/create?' |
|
107 |
'name=RDV&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&attendeePW=6256459f2baead794d599a1ad21a07fd4bc5743a' |
|
108 |
'&moderatorPW=2ebb4732fcbdd5a9f0751c3ffa7a5f51f755f980&checksum=4ac7d65fe6beb6f86fa5a13d6d8e0d1fadee0b50' |
|
109 |
) |
|
110 |
assert meeting.meeting_name == MEETING_NAME |
|
111 |
assert meeting.meeting_id == MEETING_ID |
|
112 |
assert meeting.attributes == {} |
|
113 |
assert meeting.message == {'message': 'ok'} |
|
114 | ||
115 |
class TestGetMeetingInfo: |
|
116 |
@pytest.fixture |
|
117 |
def mock(self, mock): |
|
118 |
mock.return_value.content = '''<response> |
|
119 |
<returncode>SUCCESS</returncode> |
|
120 |
<meetingName>RDV</meetingName> |
|
121 |
<message>ok</message> |
|
122 |
<meetingID>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</meetingID> |
|
123 |
<metadata> |
|
124 |
<coin>1</coin> |
|
125 |
</metadata> |
|
126 |
<others> |
|
127 |
<other> |
|
128 |
<a>1</a> |
|
129 |
</other> |
|
130 |
</others> |
|
131 |
</response>''' |
|
132 |
return mock |
|
133 | ||
134 |
def test_get_meeting_info(self, bbb, mock): |
|
135 |
result = bbb.get_meeting_info(meeting_id=MEETING_ID) |
|
136 |
prepared_request = mock.call_args[0][0] |
|
137 |
assert ( |
|
138 |
prepared_request.url == 'https://example.com/bigbluebutton/api/getMeetingInfo' |
|
139 |
'?meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&checksum=4d71906c30e6e62d8b9f1a3e57233bb64126e0da' |
|
140 |
) |
|
141 |
assert result == { |
|
142 |
'meetingName': MEETING_NAME, |
|
143 |
'meetingID': MEETING_ID, |
|
144 |
'message': 'ok', |
|
145 |
'metadata': {'coin': '1'}, |
|
146 |
'others': [{'a': '1'}], |
|
147 |
} |
|
148 | ||
149 |
def test_meetings_get(self, bbb, mock): |
|
150 |
meeting = bbb.meetings.get(meeting_id=MEETING_ID) |
|
151 |
prepared_request = mock.call_args[0][0] |
|
152 |
assert ( |
|
153 |
prepared_request.url == 'https://example.com/bigbluebutton/api/getMeetingInfo' |
|
154 |
'?meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&checksum=4d71906c30e6e62d8b9f1a3e57233bb64126e0da' |
|
155 |
) |
|
156 |
assert meeting.meeting_name == MEETING_NAME |
|
157 |
assert meeting.meeting_id == MEETING_ID |
|
158 |
assert meeting.attributes == { |
|
159 |
'metadata': {'coin': '1'}, |
|
160 |
'others': [{'a': '1'}], |
|
161 |
} |
|
162 |
assert meeting.message == {'message': 'ok'} |
|
163 | ||
164 |
class TestMakeJoinURL: |
|
165 |
def test_make_join_url(self, bbb): |
|
166 |
assert bbb.make_join_url( |
|
167 |
meeting_id=MEETING_ID, full_name='John Doe', role=bbb.ROLE_MODERATOR |
|
168 |
) == ( |
|
169 |
'https://example.com/bigbluebutton/api/join?fullName=John+Doe' |
|
170 |
'&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&role=MODERATOR' |
|
171 |
'&checksum=838762e70a6fe3257ea81f7b27acbde9945acaf9' |
|
172 |
) |
|
173 | ||
174 |
def test_meeting_join_url(self, bbb): |
|
175 |
meeting = bbb.Meeting(bbb, meeting_name=None, meeting_id=MEETING_ID, create_time=1234) |
|
176 |
assert meeting.join_url(full_name='John Doe', role=bbb.ROLE_MODERATOR) == ( |
|
177 |
'https://example.com/bigbluebutton/api/join?fullName=John+Doe' |
|
178 |
'&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&role=MODERATOR' |
|
179 |
'&createTime=1234&checksum=07a7e7fd233dd213bd6dfc5332cc842afac56ba5' |
|
180 |
) |
|
181 | ||
182 |
class TestEndMeeting: |
|
183 |
@pytest.fixture |
|
184 |
def mock(self, mock): |
|
185 |
mock.return_value.content = '''<response> |
|
186 |
<returncode>SUCCESS</returncode> |
|
187 |
<messageKey>sentEndMeetingRequest</messageKey> |
|
188 |
</response>''' |
|
189 |
return mock |
|
190 | ||
191 |
def test_end_meeting(self, bbb, mock): |
|
192 |
result = bbb.end_meeting(meeting_id=MEETING_ID) |
|
193 |
prepared_request = mock.call_args[0][0] |
|
194 |
assert ( |
|
195 |
prepared_request.url == 'https://example.com/bigbluebutton/api/end' |
|
196 |
'?meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&checksum=ed94e9d091f0a105e47d91f7f0633b6f7b881698' |
|
197 |
) |
|
198 |
assert result is True |
|
199 | ||
200 |
def test_meeting_end(self, bbb, mock): |
|
201 |
meeting = bbb.Meeting(bbb, meeting_name=None, meeting_id=MEETING_ID) |
|
202 |
result = meeting.end() |
|
203 |
prepared_request = mock.call_args[0][0] |
|
204 |
assert ( |
|
205 |
prepared_request.url == 'https://example.com/bigbluebutton/api/end' |
|
206 |
'?meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&checksum=ed94e9d091f0a105e47d91f7f0633b6f7b881698' |
|
207 |
) |
|
208 |
assert result is True |
|
209 | ||
210 |
class TestEndMeetingIfNotFound: |
|
211 |
@pytest.fixture |
|
212 |
def mock(self, mock): |
|
213 |
mock.return_value.content = '''<response> |
|
214 |
<returncode>FAILED</returncode> |
|
215 |
<messageKey>notFound</messageKey> |
|
216 |
</response>''' |
|
217 |
return mock |
|
218 | ||
219 |
def test_end_meeting(self, bbb, mock): |
|
220 |
result = bbb.end_meeting(meeting_id=MEETING_ID) |
|
221 |
prepared_request = mock.call_args[0][0] |
|
222 |
assert ( |
|
223 |
prepared_request.url == 'https://example.com/bigbluebutton/api/end' |
|
224 |
'?meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&checksum=ed94e9d091f0a105e47d91f7f0633b6f7b881698' |
|
225 |
) |
|
226 |
assert result is False |
|
227 | ||
228 |
class TestGetMeetings: |
|
229 |
@pytest.fixture |
|
230 |
def mock(self, mock): |
|
231 |
mock.return_value.content = '''<response> |
|
232 |
<returncode>SUCCESS</returncode> |
|
233 |
<meetings> |
|
234 |
<meeting> |
|
235 |
<meetingName>RDV</meetingName> |
|
236 |
<message>ok</message> |
|
237 |
<meetingID>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</meetingID> |
|
238 |
<metadata> |
|
239 |
<coin>1</coin> |
|
240 |
</metadata> |
|
241 |
<others> |
|
242 |
<other> |
|
243 |
<a>1</a> |
|
244 |
</other> |
|
245 |
</others> |
|
246 |
</meeting> |
|
247 |
</meetings> |
|
248 |
</response>''' |
|
249 |
return mock |
|
250 | ||
251 |
def test_get_meetings(self, bbb, mock): |
|
252 |
result = bbb.get_meetings() |
|
253 |
prepared_request = mock.call_args[0][0] |
|
254 |
assert ( |
|
255 |
prepared_request.url |
|
256 |
== 'https://example.com/bigbluebutton/api/getMeetings?checksum=64bf453f633be1f9d2c7b0f6bed4a4702dbcc41e' |
|
257 |
) |
|
258 |
assert result == { |
|
259 |
'meetings': [ |
|
260 |
{ |
|
261 |
'meetingID': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', |
|
262 |
'meetingName': 'RDV', |
|
263 |
'message': 'ok', |
|
264 |
'metadata': {'coin': '1'}, |
|
265 |
'others': [{'a': '1'}], |
|
266 |
} |
|
267 |
] |
|
268 |
} |
|
269 | ||
270 |
def test_meetings_all(self, bbb, mock): |
|
271 |
result = list(bbb.meetings.all()) |
|
272 |
prepared_request = mock.call_args[0][0] |
|
273 |
assert ( |
|
274 |
prepared_request.url |
|
275 |
== 'https://example.com/bigbluebutton/api/getMeetings?checksum=64bf453f633be1f9d2c7b0f6bed4a4702dbcc41e' |
|
276 |
) |
|
277 |
assert len(result) == 1 |
|
278 |
meeting = result[0] |
|
279 |
assert meeting.meeting_name == MEETING_NAME |
|
280 |
assert meeting.meeting_id == MEETING_ID |
|
281 |
assert meeting.attributes == { |
|
282 |
'metadata': {'coin': '1'}, |
|
283 |
'others': [{'a': '1'}], |
|
284 |
} |
|
285 |
assert meeting.message == {'message': 'ok'} |
|
286 | ||
287 | ||
288 |
class TestAPI: |
|
289 |
@pytest.fixture(autouse=True) |
|
290 |
def stable(self, freezer): |
|
291 |
from passerelle.apps.bbb.models import Meeting |
|
292 | ||
293 |
freezer.move_to('2022-01-01T12:00:00Z') |
|
294 |
with mock.patch.object(Meeting._meta.get_field('guid'), 'default', UUID): |
|
295 |
yield None |
|
296 | ||
297 |
@pytest.fixture |
|
298 |
def meetings_create(self): |
|
299 |
with mock.patch('passerelle.apps.bbb.utils.BBB.Meetings.create') as create: |
|
300 |
yield create |
|
301 | ||
302 |
@pytest.fixture |
|
303 |
def meetings_get(self): |
|
304 |
with mock.patch('passerelle.apps.bbb.utils.BBB.Meetings.get') as get: |
|
305 |
yield get |
|
306 | ||
307 |
@pytest.fixture |
|
308 |
def is_meeting_running(self): |
|
309 |
with mock.patch('passerelle.apps.bbb.utils.BBB.is_meeting_running') as is_running: |
|
310 |
yield is_running |
|
311 | ||
312 |
class TestCreate: |
|
313 |
def test_normal(self, app, connector, meetings_create, meetings_get): |
|
314 |
meeting = bbb_utils.BBB.Meeting( |
|
315 |
None, meeting_id=MEETING_ID, meeting_name=MEETING_NAME, logout_url=LOGOUT_URL |
|
316 |
) |
|
317 |
meetings_create.return_value = meeting |
|
318 |
meetings_get.return_value = meeting |
|
319 |
response = app.post_json( |
|
320 |
f'/bbb/{SLUG}/meeting', |
|
321 |
params={ |
|
322 |
'name': MEETING_NAME, |
|
323 |
'idempotent_id': IDEMPOTENT_ID, |
|
324 |
'create_parameters/logoutUrl': LOGOUT_URL, |
|
325 |
}, |
|
326 |
) |
|
327 |
assert meetings_create.call_args == mock.call( |
|
328 |
logoutUrl=LOGOUT_URL, meeting_id=MEETING_ID, name=MEETING_NAME |
|
329 |
) |
|
330 |
assert response.json['err'] == 0 |
|
331 |
assert response.json['data'] == { |
|
332 |
'created': '2022-01-01T12:00:00Z', |
|
333 |
'updated': '2022-01-01T12:00:00Z', |
|
334 |
'guid': MEETING_ID, |
|
335 |
'idempotent_id': IDEMPOTENT_ID, |
|
336 |
'url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/', |
|
337 |
'is_running_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/', |
|
338 |
'join_agent_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/agent/2b2111/', |
|
339 |
'join_user_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/user/fb918f/', |
|
340 |
'last_time_running': None, |
|
341 |
'name': 'RDV', |
|
342 |
'running': False, |
|
343 |
'bbb_meeting_info': { |
|
344 |
'logout_url': LOGOUT_URL, |
|
345 |
}, |
|
346 |
} |
|
347 |
meeting = connector.meetings.get() |
|
348 |
assert meeting.meeting_id == MEETING_ID |
|
349 |
assert meeting.name == MEETING_NAME |
|
350 |
assert meeting.create_kwargs == {'logoutUrl': LOGOUT_URL} |
|
351 | ||
352 |
def test_error(self, app, connector, meetings_create): |
|
353 |
meetings_create.side_effect = bbb_utils.BBB.BBBError('coin') |
|
354 |
response = app.post_json( |
|
355 |
f'/bbb/{SLUG}/meeting', |
|
356 |
params={ |
|
357 |
'name': MEETING_NAME, |
|
358 |
'idempotent_id': IDEMPOTENT_ID, |
|
359 |
'create_parameters': { |
|
360 |
'logoutUrl': LOGOUT_URL, |
|
361 |
}, |
|
362 |
}, |
|
363 |
) |
|
364 |
assert response.json['err'] == 1 |
|
365 |
assert response.json['err_desc'] == 'coin' |
|
366 | ||
367 |
def test_update(self, app, connector, meetings_create, meetings_get): |
|
368 |
connector.meetings.create( |
|
369 |
idempotent_id=IDEMPOTENT_ID, |
|
370 |
name=MEETING_NAME, |
|
371 |
guid=UUID, |
|
372 |
) |
|
373 |
response = app.post_json( |
|
374 |
f'/bbb/{SLUG}/meeting', |
|
375 |
params={ |
|
376 |
'name': MEETING_NAME, |
|
377 |
'idempotent_id': IDEMPOTENT_ID, |
|
378 |
'create_parameters/logoutUrl': LOGOUT_URL, |
|
379 |
}, |
|
380 |
) |
|
381 |
assert response.json['err'] == 1 |
|
382 |
assert ( |
|
383 |
response.json['err_desc'] == 'meeting already exists with different name of create_parameters' |
|
384 |
) |
|
385 | ||
386 |
meeting = bbb_utils.BBB.Meeting( |
|
387 |
None, meeting_id=MEETING_ID, meeting_name=MEETING_NAME, logout_url=LOGOUT_URL |
|
388 |
) |
|
389 |
meetings_create.return_value = meeting |
|
390 |
meetings_get.return_value = meeting |
|
391 |
response = app.post_json( |
|
392 |
f'/bbb/{SLUG}/meeting?update=1', |
|
393 |
params={ |
|
394 |
'name': MEETING_NAME, |
|
395 |
'idempotent_id': IDEMPOTENT_ID, |
|
396 |
'create_parameters/logoutUrl': LOGOUT_URL, |
|
397 |
}, |
|
398 |
) |
|
399 |
assert response.json['err'] == 0 |
|
400 | ||
401 |
class TestGet: |
|
402 |
def test_ok(self, app, connector, meetings_get, freezer): |
|
403 |
connector.meetings.create( |
|
404 |
guid=UUID, |
|
405 |
idempotent_id=IDEMPOTENT_ID, |
|
406 |
name=MEETING_NAME, |
|
407 |
create_parameters=json.dumps({'logoutUrl': LOGOUT_URL}), |
|
408 |
) |
|
409 |
meeting = bbb_utils.BBB.Meeting( |
|
410 |
bbb=connector.bbb, |
|
411 |
meeting_id=MEETING_ID, |
|
412 |
meeting_name=MEETING_NAME, |
|
413 |
logout_url=LOGOUT_URL, |
|
414 |
create_time=1234, |
|
415 |
) |
|
416 |
meetings_get.return_value = meeting |
|
417 |
response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/') |
|
418 |
assert response.json['err'] == 0 |
|
419 |
assert response.json['data'] == { |
|
420 |
'created': '2022-01-01T12:00:00Z', |
|
421 |
'updated': '2022-01-01T12:00:00Z', |
|
422 |
'guid': MEETING_ID, |
|
423 |
'idempotent_id': IDEMPOTENT_ID, |
|
424 |
'url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/', |
|
425 |
'is_running_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/', |
|
426 |
'join_agent_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/agent/2b2111/', |
|
427 |
'join_user_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/user/fb918f/', |
|
428 |
'last_time_running': None, |
|
429 |
'name': 'RDV', |
|
430 |
'running': False, |
|
431 |
'bbb_meeting_info': { |
|
432 |
'create_time': 1234, |
|
433 |
'logout_url': LOGOUT_URL, |
|
434 |
}, |
|
435 |
} |
|
436 |
# test cache |
|
437 |
freezer.move_to(datetime.timedelta(seconds=100)) |
|
438 |
meetings_get.side_effect = bbb_utils.BBB.BBBError('boom!') |
|
439 |
response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/') |
|
440 |
assert response.json['err'] == 0 |
|
441 |
assert response.json['data'] == { |
|
442 |
'created': '2022-01-01T12:00:00Z', |
|
443 |
'updated': '2022-01-01T12:00:00Z', |
|
444 |
'guid': MEETING_ID, |
|
445 |
'idempotent_id': IDEMPOTENT_ID, |
|
446 |
'url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/', |
|
447 |
'is_running_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/', |
|
448 |
'join_agent_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/agent/2b2111/', |
|
449 |
'join_user_url': f'http://testserver/bbb/test/meeting/{MEETING_ID}/join/user/fb918f/', |
|
450 |
'last_time_running': None, |
|
451 |
'name': 'RDV', |
|
452 |
'running': False, |
|
453 |
'bbb_meeting_info': { |
|
454 |
'create_time': 1234, |
|
455 |
'logout_url': LOGOUT_URL, |
|
456 |
}, |
|
457 |
} |
|
458 | ||
459 |
class TestIsRunning: |
|
460 |
def test_ok(self, app, connector, meetings_get, is_meeting_running, freezer): |
|
461 |
connector.meetings.create( |
|
462 |
guid=UUID, |
|
463 |
idempotent_id=IDEMPOTENT_ID, |
|
464 |
name=MEETING_NAME, |
|
465 |
create_parameters=json.dumps({'logoutUrl': LOGOUT_URL}), |
|
466 |
) |
|
467 |
meeting = bbb_utils.BBB.Meeting( |
|
468 |
bbb=connector.bbb, |
|
469 |
meeting_id=MEETING_ID, |
|
470 |
meeting_name=MEETING_NAME, |
|
471 |
logout_url=LOGOUT_URL, |
|
472 |
create_time=1234, |
|
473 |
) |
|
474 |
freezer.move_to(datetime.timedelta(seconds=6)) |
|
475 |
meetings_get.return_value = meeting |
|
476 |
is_meeting_running.return_value = {'running': 'true'} |
|
477 |
response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/is-running/') |
|
478 |
assert response['Access-Control-Allow-Origin'] == '*' |
|
479 |
assert response.json['err'] == 0 |
|
480 |
assert response.json['data'] is True |
|
481 |
assert connector.meetings.get().running is True |
|
482 |
# test cache |
|
483 |
is_meeting_running.return_value = {'running': 'false'} |
|
484 |
is_meeting_running.side_effect = bbb_utils.BBB.BBBError('boom!') |
|
485 |
response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/is-running/') |
|
486 |
assert response.json['err'] == 0 |
|
487 |
assert response.json['data'] is True |
|
488 |
# test cache expired |
|
489 |
freezer.move_to(datetime.timedelta(seconds=6)) |
|
490 |
response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/is-running/') |
|
491 |
assert response.json['err'] == 0 |
|
492 |
assert response.json['data'] is True |
|
493 |
is_meeting_running.side_effect = None |
|
494 |
response = app.get(f'/bbb/{SLUG}/meeting/{MEETING_ID}/is-running/') |
|
495 |
assert response.json['err'] == 0 |
|
496 |
assert response.json['data'] is False |
|
497 | ||
498 |
class TestJoin: |
|
499 |
def test_join_user(self, app, connector, meetings_create): |
|
500 |
model_meeting = connector.meetings.create( |
|
501 |
guid=UUID, name=MEETING_NAME, create_parameters=json.dumps({'logoutUrl': LOGOUT_URL}) |
|
502 |
) |
|
503 |
meeting = bbb_utils.BBB.Meeting( |
|
504 |
bbb=connector.bbb, |
|
505 |
meeting_id=MEETING_ID, |
|
506 |
meeting_name=MEETING_NAME, |
|
507 |
logout_url=LOGOUT_URL, |
|
508 |
create_time=1234, |
|
509 |
) |
|
510 |
meetings_create.return_value = meeting |
|
511 | ||
512 |
response = app.get( |
|
513 |
f'/bbb/{SLUG}/meeting/{MEETING_ID}/join/user/{model_meeting.user_key}/', |
|
514 |
params={'full_name': 'John Doe'}, |
|
515 |
) |
|
516 |
assert meetings_create.call_args == mock.call( |
|
517 |
logoutUrl=LOGOUT_URL, meeting_id=MEETING_ID, name=MEETING_NAME |
|
518 |
) |
|
519 |
assert response.location == ( |
|
520 |
'https://example.com/bigbluebutton/api/join' |
|
521 |
'?fullName=John+Doe&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' |
|
522 |
'&role=VIEWER&createTime=1234&checksum=1851e34e269a5063605bc64589ea53ded2f9744d' |
|
523 |
) |
|
524 | ||
525 |
def test_join_agent(self, app, connector, meetings_create): |
|
526 |
model_meeting = connector.meetings.create( |
|
527 |
guid=UUID, name=MEETING_NAME, create_parameters=json.dumps({'logoutUrl': LOGOUT_URL}) |
|
528 |
) |
|
529 |
meeting = bbb_utils.BBB.Meeting( |
|
530 |
bbb=connector.bbb, |
|
531 |
meeting_id=MEETING_ID, |
|
532 |
meeting_name=MEETING_NAME, |
|
533 |
logout_url=LOGOUT_URL, |
|
534 |
create_time=1234, |
|
535 |
) |
|
536 |
meetings_create.return_value = meeting |
|
537 | ||
538 |
response = app.get( |
|
539 |
f'/bbb/{SLUG}/meeting/{MEETING_ID}/join/agent/{model_meeting.agent_key}/', |
|
540 |
params={'full_name': 'John Doe'}, |
|
541 |
) |
|
542 |
assert meetings_create.call_args == mock.call( |
|
543 |
logoutUrl=LOGOUT_URL, meeting_id=MEETING_ID, name=MEETING_NAME |
|
544 |
) |
|
545 |
assert response.location == ( |
|
546 |
'https://example.com/bigbluebutton/api/join' |
|
547 |
'?fullName=John+Doe&meetingID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' |
|
548 |
'&role=MODERATOR&createTime=1234&checksum=07a7e7fd233dd213bd6dfc5332cc842afac56ba5' |
|
549 |
) |
|
0 |
- |