Exemple #1
0
    def _times_changed(self, sender, obj, **kwargs):
        from indico.modules.events.contributions.models.contributions import Contribution
        from indico.modules.events.models.events import Event
        from indico.modules.events.sessions.models.blocks import SessionBlock

        if not hasattr(obj, 'vc_room_associations'):
            return

        if any(assoc.vc_room.type == 'zoom' and len(assoc.vc_room.events) == 1
               for assoc in obj.vc_room_associations):
            if sender == Event:
                message = _(
                    'There are one or more scheduled Zoom meetings associated with this event which were not '
                    'automatically updated.')
            elif sender == Contribution:
                message = _(
                    'There are one or more scheduled Zoom meetings associated with the contribution "{}" which '
                    ' were not automatically updated.').format(obj.title)
            elif sender == SessionBlock:
                message = _(
                    'There are one or more scheduled Zoom meetings associated with this session block which '
                    'were not automatically updated.')
            else:
                return

            flash(message, 'warning')
Exemple #2
0
def update_zoom_meeting(zoom_id, changes, is_webinar=False):
    """Update a meeting which already exists in the Zoom API.

    :param zoom_id: ID of the meeting
    :param changes: dictionary with new attribute values
    :param is_webinar: whether the call concerns a webinar (used to call the correct endpoint)
    """
    client = ZoomIndicoClient()
    try:
        if is_webinar:
            client.update_webinar(zoom_id, changes)
        else:
            client.update_meeting(zoom_id, changes)
    except HTTPError as e:
        from indico_vc_zoom.plugin import ZoomPlugin
        ZoomPlugin.logger.exception("Error updating meeting '%s': %s", zoom_id,
                                    e.response.content)

        if e.response.json()['code'] == 3001:
            # "Meeting does not exist"
            raise VCRoomNotFoundError(_('Room no longer exists in Zoom'))

        raise VCRoomError(
            _("Can't update meeting. Please contact support if the error persists."
              ))
Exemple #3
0
class VCRoomForm(VCRoomFormBase, ZoomAdvancedFormMixin):
    """Contains all information concerning a Zoom booking"""

    advanced_fields = {'show_autojoin', 'show_phone_numbers'
                       } | VCRoomFormBase.advanced_fields
    skip_fields = advanced_fields | VCRoomFormBase.conditional_fields

    description = TextAreaField(_('Description'), [DataRequired()],
                                description=_('The description of the room'))

    #owner_user = PrincipalField(_('Owner'), [DataRequired()], description=_('The owner of the room'))
    #owner_user = HiddenField(default=session.user)
    #moderation_pin = IndicoPasswordField(_('Moderation PIN'), PIN_VALIDATORS, toggle=True,
    #                                     description=_('Used to moderate the VC Room. Only digits allowed.'))
    #room_pin = IndicoPasswordField(_('Room PIN'), PIN_VALIDATORS, toggle=True,
    #                               description=_('Used to protect the access to the VC Room (leave blank for open '
    #                                             'access). Only digits allowed.'))

    def __init__(self, *args, **kwargs):
        defaults = kwargs['obj']
        if defaults.owner_user is None and defaults.owner is not None:
            defaults.owner_user = retrieve_principal(defaults.owner)
        super(VCRoomForm, self).__init__(*args, **kwargs)

    #@generated_data
    #def owner(self):
    #    return self.owner_user.data.default

    def validate_owner_user(self, field):
        if not field.data:
            raise ValidationError(_("Unable to find this user in Indico."))
Exemple #4
0
class ZoomAdvancedFormMixin(object):
    # Advanced options (per event)

    show_autojoin = BooleanField(
        _('Show Auto-join URL'),
        widget=SwitchWidget(),
        description=_("Show the auto-join URL on the event page"))
    show_phone_numbers = BooleanField(
        _('Show Phone Access numbers'),
        widget=SwitchWidget(),
        description=_("Show a link to the list of phone access numbers"))
Exemple #5
0
class UserLookupMode(str, RichEnum):
    __titles__ = {
        'all_emails': _('All emails'),
        'email_domains': _('Email domains'),
        'authenticators': _('Authenticators'),
    }

    @property
    def title(self):
        return RichEnum.title.__get__(self, type(self))

    all_emails = 'all_emails'
    email_domains = 'email_domains'
    authenticators = 'authenticators'
Exemple #6
0
    def clone_room(self, old_event_vc_room, link_object):
        vc_room = old_event_vc_room.vc_room
        is_webinar = vc_room.data.get('meeting_type', 'regular') == 'webinar'
        has_only_one_association = len(
            {assoc.event_id
             for assoc in vc_room.events}) == 1

        if has_only_one_association:
            try:
                update_zoom_meeting(
                    vc_room.data['zoom_id'], {
                        'start_time':
                        None,
                        'duration':
                        None,
                        'type': (ZoomMeetingType.recurring_webinar_no_time
                                 if is_webinar else
                                 ZoomMeetingType.recurring_meeting_no_time)
                    })
            except VCRoomNotFoundError:
                # this check is needed in order to avoid multiple flashes
                if vc_room.status != VCRoomStatus.deleted:
                    # mark room as deleted
                    vc_room.status = VCRoomStatus.deleted
                    flash(
                        _('The room "{}" no longer exists in Zoom and was removed from the event'
                          ).format(vc_room.name), 'warning')
                # no need to create an association to a room marked as deleted
                return None
        # return the new association
        return super().clone_room(old_event_vc_room, link_object)
Exemple #7
0
def get_alt_host_emails(identifiers):
    """Convert a list of identities into a list of enterprise e-mails."""
    emails = [
        find_enterprise_email(principal_from_identifier(ident))
        for ident in identifiers
    ]
    if None in emails:
        raise VCRoomError(_('Could not find Zoom user for alternative host'))
    return emails
Exemple #8
0
 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:
         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 e.response.status_code == 404:
             if has_request_context():
                 flash(_("Room didn't exist in Zoom anymore"), 'warning')
         elif e.response.status_code == 400:
             # some sort of operational error on Zoom's side, deserves a specific error message
             raise VCRoomError(
                 _('Zoom Error: "{}"').format(e.response.json()['message']))
         else:
             self.logger.error("Can't delete room")
             raise VCRoomError(_('Problem deleting room'))
    def create_room(self, vc_room, event):
        """Create a new Zoom room for an event, given a VC room.

        In order to create the Zoom room, the function will try to do so with
        all the available identities of the user based on the authenticators
        defined in Zoom plugin's settings, in that order.

        :param vc_room: VCRoom -- The VC room from which to create the Zoom
                        room
        :param event: Event -- The event to the Zoom room will be attached
        """
        client = ZoomIndicoClient(self.settings)
        #owner = retrieve_principal(vc_room.data['owner'])
        owner = session.user
        user_id = owner.email
        topic = vc_room.name
        time_zone = event.timezone
        start = event.start_dt_local
        end = event.end_dt
        topic = vc_room.data['description']
        type_meeting = 2
        host_video = self.settings.get('host_video')
        participant_video = self.settings.get('participant_video')
        join_before_host = self.settings.get('join_before_host')
        mute_upon_entry = self.settings.get('auto_mute')

        meeting_obj = client.create_meeting(
            user_id=user_id,
            type=type_meeting,
            start_time=start,
            topic=topic,
            timezone=time_zone,
            host_video=host_video,
            participant_video=participant_video,
            join_before_host=join_before_host,
            mute_upon_entry=mute_upon_entry)

        if not meeting_obj:
            raise VCRoomNotFoundError(
                _("Could not find newly created room in Zoom"))
        vc_room.data.update({
            'zoom_id': unicode(meeting_obj['id']),
            'url': meeting_obj['join_url'],
            'start_url': meeting_obj['start_url']
        })

        flag_modified(vc_room, 'data')
        vc_room.zoom_meeting = ZoomMeeting(vc_room_id=vc_room.id,
                                           meeting=meeting_obj['id'],
                                           owned_by_user=owner,
                                           url_zoom=meeting_obj['join_url'])
        self.notify_owner_start_url(vc_room)
Exemple #10
0
class VCRoomAttachForm(VCRoomAttachFormBase):
    password_visibility = IndicoRadioField(
        _('Passcode visibility'),
        description=_("Who should be able to know this meeting's passcode"),
        orientation='horizontal',
        choices=[('everyone', _('Everyone')),
                 ('logged_in', _('Logged-in users')),
                 ('registered', _('Registered participants')),
                 ('no_one', _('No one'))])
Exemple #11
0
def fetch_zoom_meeting(vc_room, client=None, is_webinar=False):
    """Fetch a Zoom meeting from the Zoom API.

    :param vc_room: The `VCRoom` object
    :param client: a `ZoomIndicoClient` object, otherwise a fresh one will be created
    :param is_webinar: whether the call concerns a webinar (used to call the correct endpoint)
    """
    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 in {400, 404}:
            # Indico will automatically mark this room as deleted
            raise VCRoomNotFoundError(
                _('This room has been deleted from Zoom'))
        else:
            from indico_vc_zoom.plugin import ZoomPlugin
            ZoomPlugin.logger.exception('Error getting Zoom Room: %s',
                                        e.response.content)
            raise VCRoomError(
                _('Problem fetching room from Zoom. Please contact support if the error persists.'
                  ))
Exemple #12
0
from wtforms.fields.core import BooleanField
from wtforms.fields.simple import TextAreaField, HiddenField
from wtforms.validators import DataRequired, Length, Optional, Regexp, ValidationError

from indico.modules.vc.forms import VCRoomAttachFormBase, VCRoomFormBase
from indico.web.forms.base import generated_data
from indico.web.forms.fields import IndicoPasswordField, PrincipalField
from indico.web.forms.widgets import SwitchWidget

from indico_vc_zoom import _
from indico_vc_zoom.util import iter_user_identities, retrieve_principal

PIN_VALIDATORS = [
    Optional(),
    Length(min=3, max=10),
    Regexp(r'^\d+$', message=_("The PIN must be a number"))
]


class ZoomAdvancedFormMixin(object):
    # Advanced options (per event)

    show_autojoin = BooleanField(
        _('Show Auto-join URL'),
        widget=SwitchWidget(),
        description=_("Show the auto-join URL on the event page"))
    show_phone_numbers = BooleanField(
        _('Show Phone Access numbers'),
        widget=SwitchWidget(),
        description=_("Show a link to the list of phone access numbers"))
Exemple #13
0
class VCRoomForm(VCRoomFormBase):
    """Contains all information concerning a Zoom booking."""

    advanced_fields = {
        'mute_audio', 'mute_host_video', 'mute_participant_video'
    } | VCRoomFormBase.advanced_fields

    skip_fields = advanced_fields | VCRoomFormBase.conditional_fields

    meeting_type = IndicoRadioField(
        _('Meeting Type'),
        description=_('The type of Zoom meeting to be created'),
        orientation='horizontal',
        choices=[('regular', _('Regular Meeting')), ('webinar', _('Webinar'))])

    host_choice = IndicoRadioField(_('Meeting Host'), [DataRequired()],
                                   choices=[('myself', _('Myself')),
                                            ('someone_else', _('Someone else'))
                                            ])

    host_user = PrincipalField(
        _('User'),
        [HiddenUnless('host_choice', 'someone_else'),
         DataRequired()])

    password = StringField(
        _('Passcode'),
        [DataRequired(), IndicoRegexp(r'^\d{8,10}$')],
        description=_('Meeting passcode (8-10 digits)'))

    password_visibility = IndicoRadioField(
        _('Passcode visibility'),
        description=_("Who should be able to know this meeting's passcode"),
        orientation='horizontal',
        choices=[('everyone', _('Everyone')),
                 ('logged_in', _('Logged-in users')),
                 ('registered', _('Registered participants')),
                 ('no_one', _('No one'))])

    mute_audio = BooleanField(
        _('Mute audio'),
        widget=SwitchWidget(),
        description=_('Participants will join the VC room muted by default '))

    mute_host_video = BooleanField(
        _('Mute video (host)'),
        widget=SwitchWidget(),
        description=_('The host will join the VC room with video disabled'))

    mute_participant_video = BooleanField(
        _('Mute video (participants)'),
        widget=SwitchWidget(),
        description=_(
            'Participants will join the VC room with video disabled'))

    waiting_room = BooleanField(
        _('Waiting room'),
        widget=SwitchWidget(),
        description=_(
            'Participants may be kept in a waiting room by the host'))

    description = TextAreaField(
        _('Description'), description=_('Optional description for this room'))

    def __init__(self, *args, **kwargs):
        defaults = kwargs['obj']
        if defaults.host_user is None and defaults.host is not None:
            host = principal_from_identifier(defaults.host)
            defaults.host_choice = 'myself' if host == session.user else 'someone_else'
            defaults.host_user = None if host == session.user else host

        allow_webinars = current_plugin.settings.get('allow_webinars')

        if allow_webinars:
            for field_name in {
                    'mute_audio', 'mute_participant_video', 'waiting_room'
            }:
                inject_validators(self, field_name,
                                  [HiddenUnless('meeting_type', 'regular')])

        super().__init__(*args, **kwargs)

        if not allow_webinars:
            del self.meeting_type

    def validate_host_choice(self, field):
        if field.data == 'myself':
            self._check_zoom_user(session.user)

    def validate_host_user(self, field):
        if self.host_choice.data == 'someone_else':
            self._check_zoom_user(field.data)

    def _check_zoom_user(self, user):
        if find_enterprise_email(user) is None:
            raise ValidationError(_('This user has no Zoom account'))

    def validate_name(self, field):
        # Duplicate names are fine on Zoom
        pass

    @generated_data
    def host(self):
        if self.host_choice is None:
            return None
        elif self.host_choice.data == 'myself':
            return session.user.identifier
        else:
            return self.host_user.data.identifier if self.host_user.data else None
Exemple #14
0
class PluginSettingsForm(VCPluginSettingsFormBase):
    support_email = EmailField(_('Zoom email support'))

    api_key = StringField(_('API KEY'), [DataRequired()])

    api_secret = StringField(_('API SECRET'), [DataRequired()])

    auto_mute = BooleanField(
        _('Auto mute'),
        widget=SwitchWidget(_('On'), _('Off')),
        description=_(
            'The Zoom clients will join the VC room muted by default '))

    host_video = BooleanField(
        _('Host Video'),
        widget=SwitchWidget(_('On'), _('Off')),
        description=_('Start video when the host joins the meeting.'))

    participant_video = BooleanField(
        _('Participant Video'),
        widget=SwitchWidget(_('On'), _('Off')),
        description=_('Start video when participants join the meeting. '))

    join_before_host = BooleanField(
        _('Join Before Host'),
        widget=SwitchWidget(_('On'), _('Off')),
        description=
        _('Allow participants to join the meeting before the host starts the meeting. Only used for scheduled or recurring meetings.'
          ))

    #indico_room_prefix = IntegerField(_('Indico tenant prefix'), [NumberRange(min=0)],
    #                                  description=_('The tenant prefix for Indico rooms created on this server'))
    #room_group_name = StringField(_("Public rooms' group name"), [DataRequired()],
    #                              description=_('Group name for public videoconference rooms created by Indico'))
    num_days_old = IntegerField(
        _('VC room age threshold'),
        [NumberRange(min=1), DataRequired()],
        description=_(
            'Number of days after an Indico event when a videoconference room is '
            'considered old'))
    max_rooms_warning = IntegerField(
        _('Max. num. VC rooms before warning'),
        [NumberRange(min=1), DataRequired()],
        description=_(
            'Maximum number of rooms until a warning is sent to the managers'))
    zoom_phone_link = URLField(
        _('ZoomVoice phone number'),
        description=_('Link to the list of ZoomVoice phone numbers'))

    creation_email_footer = TextAreaField(
        _('Creation email footer'),
        widget=CKEditorWidget(),
        description=_(
            'Footer to append to emails sent upon creation of a VC room'))
Exemple #15
0
 def validate_authenticators(self, field):
     invalid = set(field.data) - set(multipass.identity_providers)
     if invalid:
         raise ValidationError(
             _('Invalid identity providers: {}').format(
                 escape(', '.join(invalid))))
Exemple #16
0
class PluginSettingsForm(VCPluginSettingsFormBase):
    _fieldsets = [
        (_('API Credentials'), ['api_key', 'api_secret', 'webhook_token']),
        (_('Zoom Account'), [
            'user_lookup_mode', 'email_domains', 'authenticators',
            'enterprise_domain', 'allow_webinars'
        ]),
        (_('Room Settings'), [
            'mute_audio', 'mute_host_video', 'mute_participant_video',
            'join_before_host', 'waiting_room'
        ]),
        (_('Notifications'),
         ['creation_email_footer', 'send_host_url', 'notification_emails']),
        (_('Access'), ['managers', 'acl'])
    ]

    api_key = StringField(_('API Key'), [DataRequired()])

    api_secret = IndicoPasswordField(_('API Secret'), [DataRequired()],
                                     toggle=True)

    webhook_token = IndicoPasswordField(
        _('Webhook Token'),
        toggle=True,
        description=_("Specify Zoom's webhook token if you want live updates"))

    user_lookup_mode = IndicoEnumSelectField(
        _('User lookup mode'), [DataRequired()],
        enum=UserLookupMode,
        description=_('Specify how Indico should look up the zoom user that '
                      'corresponds to an Indico user.'))

    email_domains = TextListField(
        _('E-mail domains'), [
            HiddenUnless('user_lookup_mode', UserLookupMode.email_domains),
            DataRequired()
        ],
        description=
        _('List of e-mail domains which can use the Zoom API. Indico attempts '
          'to find Zoom accounts using all email addresses of a user which use '
          'those domains.'))

    authenticators = TextListField(
        _('Indico identity providers'), [
            HiddenUnless('user_lookup_mode', UserLookupMode.authenticators),
            DataRequired()
        ],
        description=
        _('Identity providers from which to get usernames. '
          'Indico queries those providers using the email addresses of the user '
          'and attempts to find Zoom accounts having an email address with the '
          'format username@enterprise-domain.'))

    enterprise_domain = StringField(
        _('Enterprise domain'), [
            HiddenUnless('user_lookup_mode', UserLookupMode.authenticators),
            DataRequired()
        ],
        description=_(
            'The domain name used together with the usernames from the Indico '
            'identity provider'))

    allow_webinars = BooleanField(
        _('Allow Webinars (Experimental)'),
        widget=SwitchWidget(),
        description=_(
            'Allow webinars to be created through Indico. Use at your own risk.'
        ))

    mute_audio = BooleanField(
        _('Mute audio'),
        widget=SwitchWidget(),
        description=_('Participants will join the VC room muted by default '))

    mute_host_video = BooleanField(
        _('Mute video (host)'),
        widget=SwitchWidget(),
        description=_('The host will join the VC room with video disabled'))

    mute_participant_video = BooleanField(
        _('Mute video (participants)'),
        widget=SwitchWidget(),
        description=_(
            'Participants will join the VC room with video disabled'))

    join_before_host = BooleanField(
        _('Join Before Host'),
        widget=SwitchWidget(),
        description=_(
            'Allow participants to join the meeting before the host starts the '
            'meeting. Only used for scheduled or recurring meetings.'))

    waiting_room = BooleanField(
        _('Waiting room'),
        widget=SwitchWidget(),
        description=_(
            'Participants may be kept in a waiting room by the host'))

    creation_email_footer = TextAreaField(
        _('Creation email footer'),
        widget=CKEditorWidget(),
        description=_(
            'Footer to append to emails sent upon creation of a VC room'))

    send_host_url = BooleanField(
        _('Send host URL'),
        widget=SwitchWidget(),
        description=_(
            'Whether to send an e-mail with the Host URL to the meeting host upon '
            'creation of a meeting'))

    def validate_authenticators(self, field):
        invalid = set(field.data) - set(multipass.identity_providers)
        if invalid:
            raise ValidationError(
                _('Invalid identity providers: {}').format(
                    escape(', '.join(invalid))))
Exemple #17
0
    def create_room(self, vc_room, event):
        """Create a new Zoom room for an event, given a VC room.

        In order to create the Zoom room, the function will try to get
        a valid e-mail address for the user in question, which can be
        use with the Zoom API.

        :param vc_room: the VC room from which to create the Zoom room
        :param event: the event to the Zoom room will be attached
        """
        client = ZoomIndicoClient()
        host = principal_from_identifier(vc_room.data['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.setdefault('meeting_type',
                                             'regular') == 'webinar'
        scheduling_args = get_schedule_args(
            link_obj) if link_obj.start_dt else {}

        try:
            settings = {
                'host_video': not vc_room.data['mute_host_video'],
            }

            kwargs = {}
            if is_webinar:
                kwargs['type'] = (ZoomMeetingType.webinar
                                  if scheduling_args else
                                  ZoomMeetingType.recurring_webinar_no_time)
                settings['alternative_hosts'] = host_email
            else:
                kwargs = {
                    'type':
                    (ZoomMeetingType.scheduled_meeting if scheduling_args else
                     ZoomMeetingType.recurring_meeting_no_time),
                    '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,
                'agenda': vc_room.data['description'],
                'password': vc_room.data['password'],
                'timezone': event.timezone,
                'settings': settings
            })
            kwargs.update(scheduling_args)
            if is_webinar:
                meeting_obj = client.create_webinar(host_email, **kwargs)
            else:
                meeting_obj = client.create_meeting(host_email, **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'
                  ))

        vc_room.data.update({
            'zoom_id':
            str(meeting_obj['id']),
            'start_url':
            meeting_obj['start_url'],
            'host':
            host.identifier,
            'alternative_hosts':
            process_alternative_hosts(meeting_obj['settings'].get(
                'alternative_hosts', ''))
        })
        vc_room.data.update(get_url_data_args(meeting_obj['join_url']))
        flag_modified(vc_room, 'data')

        # e-mail Host URL to meeting host
        if self.settings.get('send_host_url'):
            notify_host_start_url(vc_room)
Exemple #18
0
 def validate_owner_user(self, field):
     if not field.data:
         raise ValidationError(_("Unable to find this user in Indico."))
Exemple #19
0
 def _check_zoom_user(self, user):
     if find_enterprise_email(user) is None:
         raise ValidationError(_('This user has no Zoom account'))