386 lines
14 KiB
Python
386 lines
14 KiB
Python
# passerelle - uniform access to multiple data sources and services
|
|
# Copyright (C) 2022 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import hashlib
|
|
import time
|
|
import uuid
|
|
|
|
from django.core.cache import cache
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.db import models, transaction
|
|
from django.db.models import JSONField
|
|
from django.http import HttpResponseRedirect, JsonResponse
|
|
from django.shortcuts import get_object_or_404
|
|
from django.urls import reverse
|
|
from django.utils.functional import cached_property
|
|
from django.utils.timezone import now
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from passerelle.base.models import BaseResource, HTTPResource
|
|
from passerelle.utils.api import endpoint
|
|
from passerelle.utils.jsonresponse import APIError
|
|
|
|
from . import utils
|
|
|
|
|
|
def complete_url(request, data):
|
|
data = dict(data)
|
|
for key in data:
|
|
if key == 'url' or key.endswith('_url') and data[key]:
|
|
data[key] = request.build_absolute_uri(data[key])
|
|
return data
|
|
|
|
|
|
class Resource(BaseResource, HTTPResource):
|
|
bbb_url = models.URLField(
|
|
max_length=400,
|
|
verbose_name=_('BBB URL'),
|
|
help_text=_('Base URL of Big Blue Button (use "bbb-conf --secret" to get it)'),
|
|
)
|
|
shared_secret = models.CharField(
|
|
max_length=128, verbose_name=_('Shared secret'), help_text=_('Use "bbb-conf --secret" to get it.')
|
|
)
|
|
category = _('Business Process Connectors')
|
|
|
|
class Meta:
|
|
verbose_name = _('Big Blue Button')
|
|
|
|
@cached_property
|
|
def bbb(self):
|
|
return utils.BBB(url=self.bbb_url, shared_secret=self.shared_secret, session=self.requests)
|
|
|
|
def check_status(self):
|
|
self.bbb.get_meetings()
|
|
|
|
@endpoint(
|
|
methods=['post'],
|
|
name='meeting',
|
|
perm='can_access',
|
|
description_post=_('Create a meeting'),
|
|
post={
|
|
'request_body': {
|
|
'schema': {
|
|
'application/json': {
|
|
'type': 'object',
|
|
'properties': {
|
|
'name': {
|
|
'type': 'string',
|
|
},
|
|
'idempotent_id': {
|
|
'type': 'string',
|
|
},
|
|
'create_parameters': {
|
|
'type': 'object',
|
|
'properties': {
|
|
'logoutURL': {'type': 'string'},
|
|
},
|
|
'additionalProperties': True,
|
|
},
|
|
'user_full_name': {'type': 'string'},
|
|
'join_user_parameters': {
|
|
'type': 'object',
|
|
'properties': {
|
|
'guest': {'type': 'string'},
|
|
},
|
|
'additionalProperties': True,
|
|
},
|
|
'agent_full_name': {'type': 'string'},
|
|
'join_agent_parameters': {
|
|
'type': 'object',
|
|
'properties': {
|
|
'guest': {'type': 'string'},
|
|
},
|
|
'additionalProperties': True,
|
|
},
|
|
'metadata': {
|
|
'type': 'object',
|
|
},
|
|
},
|
|
'required': ['name', 'idempotent_id'],
|
|
'unflatten': True,
|
|
}
|
|
}
|
|
}
|
|
},
|
|
)
|
|
@transaction.atomic
|
|
def meetings_endpoint(self, request, post_data):
|
|
defaults = {
|
|
key: post_data.get(key)
|
|
for key in [
|
|
'name',
|
|
'create_parameters',
|
|
'user_full_name',
|
|
'join_user_parameters',
|
|
'agent_full_name',
|
|
'join_agent_parameters',
|
|
'metadata',
|
|
]
|
|
}
|
|
meeting, created = Meeting.objects.select_for_update().get_or_create(
|
|
resource=self, idempotent_id=post_data['idempotent_id'], defaults=defaults
|
|
)
|
|
if not created:
|
|
for key, value in defaults.items():
|
|
if getattr(meeting, key, None) != value:
|
|
raise APIError('meeting already exists with different parameters')
|
|
try:
|
|
meeting.create()
|
|
except utils.BBB.BBBError as e:
|
|
raise APIError(e)
|
|
return {'data': complete_url(request, meeting.to_json())}
|
|
|
|
@endpoint(
|
|
methods=['get', 'delete'],
|
|
name='meeting',
|
|
perm='can_access',
|
|
pattern=r'^(?P<guid>[0-9a-f]{32})/?$',
|
|
example_pattern='{guid}/',
|
|
description_post=_('Get a meeting'),
|
|
parameters={
|
|
'guid': {
|
|
'description': _('Meeting guid'),
|
|
'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
|
|
},
|
|
},
|
|
)
|
|
def meeting_endpoint(self, request, guid):
|
|
if request.method == 'DELETE':
|
|
with transaction.atomic():
|
|
meeting = get_object_or_404(Meeting.objects.select_for_update(), guid=guid)
|
|
try:
|
|
meeting.create().end()
|
|
except Exception:
|
|
pass
|
|
meeting.delete()
|
|
return {}
|
|
else:
|
|
meeting = get_object_or_404(Meeting.objects, guid=guid)
|
|
return {'data': complete_url(request, meeting.to_json())}
|
|
|
|
@endpoint(
|
|
methods=['get'],
|
|
name='meeting',
|
|
perm='OPEN',
|
|
pattern=r'^(?P<guid>[0-9a-f]{32})/is-running/?$',
|
|
example_pattern='{guid}/is-running/',
|
|
description_post=_('Report if meeting is running'),
|
|
parameters={
|
|
'guid': {
|
|
'description': _('Meeting guid'),
|
|
'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
|
|
},
|
|
},
|
|
)
|
|
@transaction.atomic
|
|
def is_running(self, request, guid):
|
|
meeting = get_object_or_404(Meeting.objects.select_for_update(), guid=guid)
|
|
if (now() - meeting.updated).total_seconds() > 5:
|
|
meeting.update_is_running()
|
|
response = JsonResponse({'err': 0, 'data': meeting.running})
|
|
response['Access-Control-Allow-Origin'] = '*'
|
|
return response
|
|
|
|
@endpoint(
|
|
methods=['get'],
|
|
name='meeting',
|
|
perm='OPEN',
|
|
pattern=r'^(?P<guid>[0-9a-f]{32})/join/agent/(?P<key>[^/]*)/?$',
|
|
example_pattern='{guid}/join/agent/',
|
|
description_post=_('Get a meeting'),
|
|
parameters={
|
|
'guid': {
|
|
'description': _('Meeting guid'),
|
|
'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
|
|
},
|
|
'full_name': {
|
|
'description': _('Agent full name'),
|
|
'example_value': 'John Doe',
|
|
},
|
|
'key': {
|
|
'description': _('Secret key'),
|
|
'example_value': '1234',
|
|
},
|
|
},
|
|
)
|
|
def join_agent(self, request, guid, key, full_name=None):
|
|
meeting = get_object_or_404(Meeting, guid=guid)
|
|
if key != meeting.agent_key:
|
|
raise PermissionDenied
|
|
return HttpResponseRedirect(meeting.make_join_agent_url(full_name))
|
|
|
|
@endpoint(
|
|
methods=['get'],
|
|
name='meeting',
|
|
perm='OPEN',
|
|
pattern=r'^(?P<guid>[0-9a-f]{32})/join/user/(?P<key>[^/]*)/?$',
|
|
example_pattern='{guid}/join/user/',
|
|
description_post=_('Get a meeting'),
|
|
parameters={
|
|
'guid': {
|
|
'description': _('Meeting guid'),
|
|
'example_value': '7edb43abf2004f55a8a526ac4b1403e4',
|
|
},
|
|
'full_name': {
|
|
'description': _('User full name'),
|
|
'example_value': 'John Doe',
|
|
},
|
|
'key': {
|
|
'description': _('Secret key'),
|
|
'example_value': '1234',
|
|
},
|
|
},
|
|
)
|
|
def join_user(self, request, guid, key, full_name=None):
|
|
meeting = get_object_or_404(Meeting, guid=guid)
|
|
if key != meeting.user_key:
|
|
raise PermissionDenied
|
|
return HttpResponseRedirect(meeting.make_join_user_url(full_name))
|
|
|
|
def _make_endpoint_url(self, endpoint, rest):
|
|
return reverse(
|
|
'generic-endpoint',
|
|
kwargs={'connector': 'bbb', 'endpoint': endpoint, 'slug': self.slug, 'rest': rest},
|
|
)
|
|
|
|
|
|
class Meeting(models.Model):
|
|
created = models.DateTimeField(verbose_name=_('Created'), auto_now_add=True)
|
|
updated = models.DateTimeField(verbose_name=_('Updated'), auto_now=True)
|
|
resource = models.ForeignKey(
|
|
Resource, verbose_name=_('Resource'), on_delete=models.CASCADE, related_name='meetings'
|
|
)
|
|
guid = models.UUIDField(verbose_name=_('UUID'), unique=True, default=uuid.uuid4)
|
|
name = models.TextField(verbose_name=_('Name'))
|
|
idempotent_id = models.TextField(verbose_name=_('Idempotent ID'), unique=True)
|
|
running = models.BooleanField(verbose_name=_('Is running?'), default=False)
|
|
last_time_running = models.DateTimeField(verbose_name=_('Last time running'), null=True)
|
|
create_parameters = JSONField('Create parameters', null=True)
|
|
user_full_name = models.TextField(_('User full name'), null=True)
|
|
agent_full_name = models.TextField(_('Agent full name'), null=True)
|
|
join_user_parameters = JSONField('Join user parameters', null=True)
|
|
join_agent_parameters = JSONField('Join agent parameters', null=True)
|
|
metadata = JSONField('Metadata', null=True)
|
|
|
|
def _make_key(self, _for):
|
|
return hashlib.sha1((self.resource.shared_secret + _for + self.meeting_id).encode()).hexdigest()[:6]
|
|
|
|
@property
|
|
def bbb(self):
|
|
return self.resource.bbb
|
|
|
|
@property
|
|
def user_key(self):
|
|
return self._make_key('user')
|
|
|
|
@property
|
|
def join_user_url(self):
|
|
return self.resource._make_endpoint_url(
|
|
endpoint='meeting', rest=f'{self.guid.hex}/join/user/{self.user_key}/'
|
|
)
|
|
|
|
@property
|
|
def agent_key(self):
|
|
return self._make_key('agent')
|
|
|
|
@property
|
|
def join_agent_url(self):
|
|
return self.resource._make_endpoint_url(
|
|
endpoint='meeting', rest=f'{self.guid.hex}/join/agent/{self.agent_key}/'
|
|
)
|
|
|
|
@property
|
|
def url(self):
|
|
return self.resource._make_endpoint_url(endpoint='meeting', rest=f'{self.guid.hex}/')
|
|
|
|
@property
|
|
def is_running_url(self):
|
|
return self.resource._make_endpoint_url(endpoint='meeting', rest=f'{self.guid.hex}/is-running/')
|
|
|
|
def to_json(self):
|
|
return {
|
|
'guid': self.guid.hex,
|
|
'created': self.created,
|
|
'updated': self.updated,
|
|
'name': self.name,
|
|
'idempotent_id': self.idempotent_id,
|
|
'running': self.running,
|
|
'last_time_running': self.last_time_running,
|
|
'url': self.url,
|
|
'join_user_url': self.join_user_url,
|
|
'join_agent_url': self.join_agent_url,
|
|
'is_running_url': self.is_running_url,
|
|
'bbb_meeting_info': self.meeting_info(),
|
|
'create_parameters': self.create_parameters,
|
|
'join_user_parameters': self.join_user_parameters,
|
|
'join_agent_parameters': self.join_agent_parameters,
|
|
'metadata': self.metadata,
|
|
}
|
|
|
|
@property
|
|
def meeting_id(self):
|
|
return self.guid.hex
|
|
|
|
def update_is_running(self):
|
|
try:
|
|
running = self.resource.bbb.is_meeting_running(self.meeting_id)
|
|
except self.resource.bbb.BBBError:
|
|
return
|
|
if self.running != running:
|
|
self.running = running
|
|
if running:
|
|
self.last_time_running = now()
|
|
self.save(update_fields=['updated', 'last_time_running', 'running'])
|
|
else:
|
|
self.save(update_fields=['updated', 'running'])
|
|
|
|
def create(self):
|
|
return self.resource.bbb.meetings.create(
|
|
name=self.name, meeting_id=self.meeting_id, **(self.create_parameters or {})
|
|
)
|
|
|
|
@property
|
|
def cache_key(self):
|
|
return f'bbb_{self.resource.slug}_{self.guid.hex}'
|
|
|
|
def meeting_info(self):
|
|
start = time.time()
|
|
try:
|
|
data, timestamp = cache.get(self.cache_key)
|
|
except TypeError:
|
|
data = None
|
|
if data is None or (start - timestamp) > 30:
|
|
try:
|
|
data = self.resource.bbb.meetings.get(meeting_id=self.meeting_id).attributes
|
|
except utils.BBB.BBBError:
|
|
data = data or {}
|
|
cache.set(self.cache_key, (data, start))
|
|
return data
|
|
|
|
def make_join_agent_url(self, full_name):
|
|
return self.create().join_url(
|
|
str(full_name or self.agent_full_name or _('Agent')),
|
|
self.bbb.ROLE_MODERATOR,
|
|
**(self.join_agent_parameters or {}),
|
|
)
|
|
|
|
def make_join_user_url(self, full_name):
|
|
return self.create().join_url(
|
|
str(full_name or self.user_full_name or _('User')),
|
|
self.bbb.ROLE_VIEWER,
|
|
**(self.join_user_parameters or {}),
|
|
)
|