VC/Zoom: Add webinar mode

This commit is contained in:
Pedro Ferreira 2020-09-30 09:24:13 +02:00
parent 6525135348
commit 354df7b1d7
5 changed files with 170 additions and 43 deletions

View File

@ -83,8 +83,35 @@ class MeetingComponent(BaseComponent):
"{}/meetings/{}".format(self.base_uri, meeting_id), json=kwargs
)
def get_invitation(self, meeting_id, **kwargs):
return self.session.get("{}/meetings/{}/invitation".format(self.base_uri, meeting_id), json=kwargs)
class WebinarComponent(BaseComponent):
def list(self, user_id, **kwargs):
return self.get(
"{}/users/{}/webinars".format(self.base_uri, user_id), params=kwargs
)
def create(self, user_id, **kwargs):
if kwargs.get("start_time"):
kwargs["start_time"] = format_iso_dt(kwargs["start_time"])
return self.session.post(
"{}/users/{}/webinars".format(self.base_uri, user_id),
json=kwargs
)
def get(self, meeting_id, **kwargs):
return self.session.get("{}/webinars/{}".format(self.base_uri, meeting_id), json=kwargs)
def update(self, meeting_id, **kwargs):
if kwargs.get("start_time"):
kwargs["start_time"] = format_iso_dt(kwargs["start_time"])
return self.session.patch(
"{}/webinars/{}".format(self.base_uri, meeting_id), json=kwargs
)
def delete(self, meeting_id, **kwargs):
return self.session.delete(
"{}/webinars/{}".format(self.base_uri, meeting_id), json=kwargs
)
class UserComponent(BaseComponent):
@ -118,7 +145,8 @@ class ZoomClient(object):
_components = {
'user': UserComponent,
'meeting': MeetingComponent
'meeting': MeetingComponent,
'webinar': WebinarComponent
}
def __init__(self, api_key, api_secret, timeout=15):
@ -180,12 +208,21 @@ class ZoomIndicoClient(object):
def update_meeting(self, meeting_id, data):
return _handle_response(self.client.meeting.update(meeting_id, **data), 204, expects_json=False)
def get_meeting_invitation(self, meeting_id):
return _handle_response(self.client.meeting.get_invitation(meeting_id))
def delete_meeting(self, meeting_id):
return _handle_response(self.client.meeting.delete(meeting_id), 204, expects_json=False)
def create_webinar(self, user_id, **kwargs):
return _handle_response(self.client.webinar.create(user_id, **kwargs), 201)
def get_webinar(self, webinar_id):
return _handle_response(self.client.webinar.get(webinar_id))
def update_webinar(self, webinar_id, data):
return _handle_response(self.client.webinar.update(webinar_id, **data), 204, expects_json=False)
def delete_webinar(self, webinar_id):
return _handle_response(self.client.webinar.delete(webinar_id), 204, expects_json=False)
def check_user_meeting_time(self, user_id, start_dt, end_dt):
pass

View File

@ -32,6 +32,13 @@ class VCRoomForm(VCRoomFormBase):
skip_fields = advanced_fields | VCRoomFormBase.conditional_fields
meeting_type = IndicoRadioField(_("Meeting Type"),
description=_("The type of Zoom meeting ot be created"),
orientation='horizontal',
choices=[
('regular', _('Regular Meeting')),
('webinar', _('Webinar'))])
host_choice = IndicoRadioField(_("Meeting Host"), [DataRequired()],
choices=[('myself', _("Myself")), ('someone_else', _("Someone else"))])
@ -47,6 +54,7 @@ class VCRoomForm(VCRoomFormBase):
('no_one', _("No one"))])
mute_audio = BooleanField(_('Mute audio'),
[HiddenUnless('meeting_type', 'regular')],
widget=SwitchWidget(),
description=_('Participants will join the VC room muted by default '))
@ -55,14 +63,16 @@ class VCRoomForm(VCRoomFormBase):
description=_('The host will join the VC room with video disabled'))
mute_participant_video = BooleanField(_('Mute video (participants)'),
[HiddenUnless('meeting_type', 'regular')],
widget=SwitchWidget(),
description=_('Participants will join the VC room with video disabled'))
waiting_room = BooleanField(_('Waiting room'),
[HiddenUnless('meeting_type', 'regular')],
widget=SwitchWidget(),
description=_('Participants may be kept in a waiting room by the host'))
description = TextAreaField(_('Description'), description=_('The description of the room'))
description = TextAreaField(_('Description'), description=_('Optional description for this room'))
def __init__(self, *args, **kwargs):
defaults = kwargs['obj']
@ -74,6 +84,8 @@ class VCRoomForm(VCRoomFormBase):
@generated_data
def host(self):
if self.host_choice is None:
return None
return session.user.identifier if self.host_choice.data == 'myself' else self.host_user.data.identifier
def validate_host_user(self, field):

View File

@ -15,12 +15,13 @@ from wtforms.validators import DataRequired, NumberRange
from indico.core import signals
from indico.core.config import config
from indico.core.plugins import IndicoPlugin, url_for_plugin
from indico.core.plugins import IndicoPlugin, render_plugin_template, url_for_plugin
from indico.modules.events.views import WPSimpleEventDisplay
from indico.modules.vc import VCPluginMixin, VCPluginSettingsFormBase
from indico.modules.vc.exceptions import VCRoomError, VCRoomNotFoundError
from indico.modules.vc.models.vc_rooms import VCRoom
from indico.modules.vc.views import WPVCEventPage, WPVCManageEvent
from indico.util.date_time import now_utc
from indico.util.user import principal_from_identifier
from indico.web.forms.fields.simple import IndicoPasswordField
from indico.web.forms.widgets import CKEditorWidget, SwitchWidget
@ -40,12 +41,14 @@ def _gen_random_password():
return ''.join(random.sample(string.ascii_lowercase + string.ascii_uppercase + string.digits, 10))
def _fetch_zoom_meeting(vc_room, client=None):
def _fetch_zoom_meeting(vc_room, client=None, is_webinar=False):
try:
client = client or ZoomIndicoClient()
if is_webinar:
return client.get_webinar(vc_room.data['zoom_id'])
return client.get_meeting(vc_room.data['zoom_id'])
except HTTPError as e:
if e.response.status_code == 404:
if e.response.status_code in {400, 404}:
# Indico will automatically mark this room as deleted
raise VCRoomNotFoundError(_("This room has been deleted from Zoom"))
else:
@ -53,19 +56,27 @@ def _fetch_zoom_meeting(vc_room, client=None):
raise VCRoomError(_("Problem fetching room from Zoom. Please contact support if the error persists."))
def _update_zoom_meeting(zoom_id, changes):
def _update_zoom_meeting(zoom_id, changes, is_webinar=False):
client = ZoomIndicoClient()
try:
client.update_meeting(zoom_id, changes)
if is_webinar:
client.update_webinar(zoom_id, changes)
else:
client.update_meeting(zoom_id, changes)
except HTTPError as e:
ZoomPlugin.logger.exception("Error updating meeting '%s': %s", zoom_id, e.response.content)
raise VCRoomError(_("Can't update meeting. Please contact support if the error persists."))
def _get_schedule_args(obj):
duration = obj.end_dt - obj.start_dt
if obj.start_dt < now_utc():
return {}
return {
'start_time': obj.start_dt,
'duration': (obj.end_dt - obj.start_dt).total_seconds() / 60,
'duration': duration.total_seconds() / 60,
}
@ -136,6 +147,7 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin):
super(ZoomPlugin, self).init()
self.connect(signals.plugin.cli, self._extend_indico_cli)
self.connect(signals.event.times_changed, self._times_changed)
self.template_hook('event-vc-room-list-item-labels', self._render_vc_room_labels)
self.inject_bundle('main.js', WPSimpleEventDisplay)
self.inject_bundle('main.js', WPVCEventPage)
self.inject_bundle('main.js', WPVCManageEvent)
@ -169,6 +181,25 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin):
def icon_url(self):
return url_for_plugin(self.name + '.static', filename='images/zoom_logo.png')
def create_form(self, event, existing_vc_room=None, existing_event_vc_room=None):
"""Override the default room form creation mechanism."""
form = super(ZoomPlugin, self).create_form(
event,
existing_vc_room=existing_vc_room,
existing_event_vc_room=existing_event_vc_room
)
if existing_vc_room:
# if we're editing a VC room, we will not allow the meeting type to be changed
form.meeting_type.render_kw = {'disabled': True}
if form.data['meeting_type'] == 'webinar':
# webinar hosts cannot be changed through the API
form.host_choice.render_kw = {'disabled': True}
form.host_user.render_kw = {'disabled': True}
return form
def _extend_indico_cli(self, sender, **kwargs):
return cli
@ -200,11 +231,20 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin):
room_assoc.data['password_visibility'] = data.pop('password_visibility')
flag_modified(room_assoc, 'data')
def update_data_vc_room(self, vc_room, data):
def update_data_vc_room(self, vc_room, data, is_new=False):
super(ZoomPlugin, self).update_data_vc_room(vc_room, data)
fields = {'description'}
if data['meeting_type'] == 'webinar':
fields |= {'mute_host_video'}
if is_new:
fields |= {'host', 'meeting_type'}
else:
fields |= {
'meeting_type', 'host', 'mute_audio', 'mute_participant_video', 'mute_host_video', 'join_before_host',
'waiting_room'
}
for key in {'description', 'host', 'mute_audio', 'mute_participant_video', 'mute_host_video',
'join_before_host', 'waiting_room'}:
for key in fields:
if key in data:
vc_room.data[key] = data.pop(key)
@ -237,32 +277,50 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin):
"""
client = ZoomIndicoClient()
host = principal_from_identifier(vc_room.data['host'])
host_id = find_enterprise_email(host)
host_email = find_enterprise_email(host)
# get the object that this booking is linked to
vc_room_assoc = vc_room.events[0]
link_obj = vc_room_assoc.link_object
is_webinar = vc_room.data['meeting_type'] == 'webinar'
scheduling_args = _get_schedule_args(link_obj) if link_obj.start_dt else {}
self._check_indico_is_assistant(host_id)
self._check_indico_is_assistant(host_email)
try:
settings = {
'host_video': vc_room.data['mute_host_video'],
'participant_video': not vc_room.data['mute_participant_video'],
'join_before_host': self.settings.get('join_before_host'),
'mute_upon_entry': vc_room.data['mute_audio'],
'waiting_room': vc_room.data['waiting_room']
}
meeting_obj = client.create_meeting(self.settings.get('assistant_id'),
type=2 if scheduling_args else 3, # scheduled vs. recurring meeting
topic=vc_room.name,
password=_gen_random_password(),
schedule_for=host_id,
timezone=event.timezone,
settings=settings,
**scheduling_args)
kwargs = {}
if is_webinar:
kwargs = {
'type': 5 if scheduling_args else 6,
'host_email': host_email
}
else:
kwargs = {
'type': 2 if scheduling_args else 3,
'schedule_for': host_email
}
settings.update({
'mute_upon_entry': vc_room.data['mute_audio'],
'participant_video': not vc_room.data['mute_participant_video'],
'waiting_room': vc_room.data['waiting_room'],
'join_before_host': self.settings.get('join_before_host'),
})
kwargs.update({
'topic': vc_room.name,
'password': _gen_random_password(),
'timezone': event.timezone,
'settings': settings
})
kwargs.update(scheduling_args)
if is_webinar:
meeting_obj = client.create_webinar(self.settings.get('assistant_id'), **kwargs)
else:
meeting_obj = client.create_meeting(self.settings.get('assistant_id'), **kwargs)
except HTTPError as e:
self.logger.exception('Error creating Zoom Room: %s', e.response.content)
raise VCRoomError(_("Could not create the room in Zoom. Please contact support if the error persists"))
@ -284,7 +342,8 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin):
def update_room(self, vc_room, event):
client = ZoomIndicoClient()
zoom_meeting = _fetch_zoom_meeting(vc_room, client=client)
is_webinar = vc_room.data['meeting_type'] == 'webinar'
zoom_meeting = _fetch_zoom_meeting(vc_room, client=client, is_webinar=is_webinar)
changes = {}
host = principal_from_identifier(vc_room.data['host'])
@ -303,7 +362,7 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin):
if not email:
raise Forbidden(_("This user doesn't seem to have an associated Zoom account"))
changes['schedule_for'] = email
changes['host_email' if is_webinar else 'schedule_for'] = email
self._check_indico_is_assistant(email)
notify_new_host(session.user, vc_room)
@ -311,20 +370,23 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin):
changes['topic'] = vc_room.name
zoom_meeting_settings = zoom_meeting['settings']
if vc_room.data['mute_audio'] != zoom_meeting_settings['mute_upon_entry']:
changes.setdefault('settings', {})['mute_upon_entry'] = vc_room.data['mute_audio']
if vc_room.data['mute_participant_video'] == zoom_meeting_settings['participant_video']:
changes.setdefault('settings', {})['participant_video'] = not vc_room.data['mute_participant_video']
if vc_room.data['mute_host_video'] == zoom_meeting_settings['host_video']:
changes.setdefault('settings', {})['host_video'] = not vc_room.data['mute_host_video']
if vc_room.data['waiting_room'] != zoom_meeting_settings['waiting_room']:
changes.setdefault('settings', {})['waiting_room'] = vc_room.data['waiting_room']
if not is_webinar:
if vc_room.data['mute_audio'] != zoom_meeting_settings['mute_upon_entry']:
changes.setdefault('settings', {})['mute_upon_entry'] = vc_room.data['mute_audio']
if vc_room.data['mute_participant_video'] == zoom_meeting_settings['participant_video']:
changes.setdefault('settings', {})['participant_video'] = not vc_room.data['mute_participant_video']
if vc_room.data['waiting_room'] != zoom_meeting_settings['waiting_room']:
changes.setdefault('settings', {})['waiting_room'] = vc_room.data['waiting_room']
if changes:
_update_zoom_meeting(vc_room.data['zoom_id'], changes)
_update_zoom_meeting(vc_room.data['zoom_id'], changes, is_webinar=is_webinar)
def refresh_room(self, vc_room, event):
zoom_meeting = _fetch_zoom_meeting(vc_room)
is_webinar = vc_room.data['meeting_type'] == 'webinar'
zoom_meeting = _fetch_zoom_meeting(vc_room, is_webinar=is_webinar)
vc_room.name = zoom_meeting['topic']
vc_room.data.update({
'url': zoom_meeting['join_url'],
@ -336,8 +398,12 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin):
def delete_room(self, vc_room, event):
client = ZoomIndicoClient()
zoom_id = vc_room.data['zoom_id']
is_webinar = vc_room.data['meeting_type'] == 'webinar'
try:
client.delete_meeting(zoom_id)
if is_webinar:
client.delete_webinar(zoom_id)
else:
client.delete_meeting(zoom_id)
except HTTPError as e:
# if there's a 404, there is no problem, since the room is supposed to be gone anyway
if not e.response.status_code == 404:
@ -350,6 +416,7 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin):
def get_vc_room_form_defaults(self, event):
defaults = super(ZoomPlugin, self).get_vc_room_form_defaults(event)
defaults.update({
'meeting_type': 'regular',
'mute_audio': self.settings.get('mute_audio'),
'mute_host_video': self.settings.get('mute_host_video'),
'mute_participant_video': self.settings.get('mute_participant_video'),
@ -382,6 +449,11 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin):
def get_notification_cc_list(self, action, vc_room, event):
return {principal_from_identifier(vc_room.data['host']).email}
def _render_vc_room_labels(self, event, vc_room, **kwargs):
if vc_room.plugin != self:
return
return render_plugin_template('room_labels.html', vc_room=vc_room)
def _times_changed(self, sender, obj, **kwargs):
from indico.modules.events.models.events import Event
from indico.modules.events.contributions.models.contributions import Contribution

View File

@ -2,7 +2,7 @@
{% set host = vc_room.data.host %}
{% set phone_link = settings.get('zoom_phone_link') %}
<dl>
<dt>{% trans %}Meeting ID{% endtrans %}</dt>
<dt>{% trans %}Zoom Meeting ID{% endtrans %}</dt>
<dd>{{ vc_room.data.zoom_id }}</dd>
{% if host %}
<dt>{% trans %}Host{% endtrans %}</dt>

View File

@ -0,0 +1,6 @@
{% if vc_room.data.meeting_type == 'webinar' %}
<div class="i-label color-teal text-color outline"
title="{% trans %}This is a Zoom webinar{% endtrans %}">
{% trans %}Webinar{% endtrans %}
</div>
{% endif %}