diff --git a/vc_zoom/README.md b/vc_zoom/README.md index f819585..de3223d 100644 --- a/vc_zoom/README.md +++ b/vc_zoom/README.md @@ -28,6 +28,8 @@ - Remove the "Assistant Zoom ID" logic due to problems with Zoom's API rate limits (all meetings created were counted against the assistant's rate limit instead of the host's); this means the host can no longer be changed, but Indico instead provides an option to event managers to make themselves a co-host. - Fix an error when changing the linked object of a recurring Zoom room in Indico - Include Zoom join links in the event's ical export (note: only Zoom meetings with a public passcode are displayed unless you have at least Indico v2.3.3) +- Skip deleted Zoom meetings when cloning events +- Mark Zoom meetings as deleted when receiving the corresponding webhook event **Breaking change:** The email domains are now stored as a list of strings instead of a comma-separated list. You need to update them in the plugin settings. diff --git a/vc_zoom/indico_vc_zoom/controllers.py b/vc_zoom/indico_vc_zoom/controllers.py index d3177d5..6684729 100644 --- a/vc_zoom/indico_vc_zoom/controllers.py +++ b/vc_zoom/indico_vc_zoom/controllers.py @@ -15,7 +15,7 @@ from werkzeug.exceptions import Forbidden from indico.core.db import db from indico.modules.vc.controllers import RHVCSystemEventBase from indico.modules.vc.exceptions import VCRoomError -from indico.modules.vc.models.vc_rooms import VCRoom +from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomStatus from indico.util.i18n import _ from indico.web.rh import RH @@ -49,8 +49,8 @@ class RHWebhook(RH): raise Forbidden @use_kwargs({ - 'event': fields.String(), - 'payload': fields.Dict() + 'event': fields.String(required=True), + 'payload': fields.Dict(required=True) }) def _process(self, event, payload): meeting_id = payload['object']['id'] @@ -61,7 +61,10 @@ class RHWebhook(RH): current_plugin.logger.debug('Action for unhandled Zoom room: %s', meeting_id) return - if event in {'meeting.updated', 'webinar.updated', 'meeting.deleted', 'webinar.deleted'}: + if event in ('meeting.updated', 'webinar.updated'): current_plugin.refresh_room(vc_room, None) + elif event in ('meeting.deleted', 'webinar.deleted'): + current_plugin.logger.info('Zoom room deleted: %s', meeting_id) + vc_room.status = VCRoomStatus.deleted else: current_plugin.logger.warning('Unhandled Zoom webhook payload: %s', event) diff --git a/vc_zoom/indico_vc_zoom/plugin.py b/vc_zoom/indico_vc_zoom/plugin.py index 46ff64e..2945639 100644 --- a/vc_zoom/indico_vc_zoom/plugin.py +++ b/vc_zoom/indico_vc_zoom/plugin.py @@ -16,11 +16,12 @@ from wtforms.validators import DataRequired, ValidationError from indico.core import signals from indico.core.auth import multipass +from indico.core.errors import UserValueError 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 -from indico.modules.vc.models.vc_rooms import VCRoom +from indico.modules.vc.exceptions import VCRoomError, VCRoomNotFoundError +from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomStatus from indico.modules.vc.views import WPVCEventPage, WPVCManageEvent from indico.util.user import principal_from_identifier from indico.web.forms.fields import IndicoEnumSelectField, IndicoPasswordField, TextListField @@ -205,38 +206,41 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin): super().update_data_association(event, vc_room, room_assoc, data) if vc_room.data: - # this is not a new room - if association_is_new: - # this means we are updating an existing meeting with a new vc_room-event association - 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 - ) - }) - elif room_assoc.link_object != old_link: - # the booking should now be linked to something else - new_schedule_args = get_schedule_args(room_assoc.link_object) if room_assoc.link_object.start_dt else {} - meeting = fetch_zoom_meeting(vc_room) - current_schedule_args = {k: meeting[k] for k in {'start_time', 'duration'} if k in meeting} + try: + # this is not a new room + if association_is_new: + # this means we are updating an existing meeting with a new vc_room-event association + 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 + ) + }) + elif room_assoc.link_object != old_link: + # the booking should now be linked to something else + new_schedule_args = get_schedule_args(room_assoc.link_object) if room_assoc.link_object.start_dt else {} + meeting = fetch_zoom_meeting(vc_room) + current_schedule_args = {k: meeting[k] for k in {'start_time', 'duration'} if k in meeting} - # check whether the start time / duration of the scheduled meeting differs - if new_schedule_args != current_schedule_args: - if new_schedule_args: - update_zoom_meeting(vc_room.data['zoom_id'], new_schedule_args) - else: - 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 - ) - }) + # check whether the start time / duration of the scheduled meeting differs + if new_schedule_args != current_schedule_args: + if new_schedule_args: + update_zoom_meeting(vc_room.data['zoom_id'], new_schedule_args) + else: + 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 as exc: + raise UserValueError(str(exc)) from exc room_assoc.data['password_visibility'] = data.pop('password_visibility') flag_modified(room_assoc, 'data') @@ -422,20 +426,31 @@ class ZoomPlugin(VCPluginMixin, IndicoPlugin): 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 - new_assoc = super().clone_room(old_event_vc_room, link_object) if has_only_one_association: - 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 - ) - }) - - return new_assoc + 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) def get_blueprints(self): return blueprint diff --git a/vc_zoom/indico_vc_zoom/translations/messages.pot b/vc_zoom/indico_vc_zoom/translations/messages.pot index 13bb658..f8aac9a 100644 --- a/vc_zoom/indico_vc_zoom/translations/messages.pot +++ b/vc_zoom/indico_vc_zoom/translations/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-01-11 10:36+0100\n" +"POT-Creation-Date: 2021-01-20 13:54+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -90,35 +90,35 @@ msgstr "" msgid "Meeting passcode (8-10 digits)" msgstr "" -#: indico_vc_zoom/forms.py:72 indico_vc_zoom/plugin.py:86 +#: indico_vc_zoom/forms.py:72 indico_vc_zoom/plugin.py:87 msgid "Mute audio" msgstr "" -#: indico_vc_zoom/forms.py:74 indico_vc_zoom/plugin.py:88 +#: indico_vc_zoom/forms.py:74 indico_vc_zoom/plugin.py:89 msgid "Participants will join the VC room muted by default " msgstr "" -#: indico_vc_zoom/forms.py:76 indico_vc_zoom/plugin.py:90 +#: indico_vc_zoom/forms.py:76 indico_vc_zoom/plugin.py:91 msgid "Mute video (host)" msgstr "" -#: indico_vc_zoom/forms.py:78 indico_vc_zoom/plugin.py:92 +#: indico_vc_zoom/forms.py:78 indico_vc_zoom/plugin.py:93 msgid "The host will join the VC room with video disabled" msgstr "" -#: indico_vc_zoom/forms.py:80 indico_vc_zoom/plugin.py:94 +#: indico_vc_zoom/forms.py:80 indico_vc_zoom/plugin.py:95 msgid "Mute video (participants)" msgstr "" -#: indico_vc_zoom/forms.py:82 indico_vc_zoom/plugin.py:96 +#: indico_vc_zoom/forms.py:82 indico_vc_zoom/plugin.py:97 msgid "Participants will join the VC room with video disabled" msgstr "" -#: indico_vc_zoom/forms.py:84 indico_vc_zoom/plugin.py:103 +#: indico_vc_zoom/forms.py:84 indico_vc_zoom/plugin.py:104 msgid "Waiting room" msgstr "" -#: indico_vc_zoom/forms.py:86 indico_vc_zoom/plugin.py:105 +#: indico_vc_zoom/forms.py:86 indico_vc_zoom/plugin.py:106 msgid "Participants may be kept in a waiting room by the host" msgstr "" @@ -135,48 +135,48 @@ msgstr "" msgid "This user has no Zoom account" msgstr "" -#: indico_vc_zoom/plugin.py:53 +#: indico_vc_zoom/plugin.py:54 msgid "API Key" msgstr "" -#: indico_vc_zoom/plugin.py:55 +#: indico_vc_zoom/plugin.py:56 msgid "API Secret" msgstr "" -#: indico_vc_zoom/plugin.py:57 +#: indico_vc_zoom/plugin.py:58 msgid "Webhook Token" msgstr "" -#: indico_vc_zoom/plugin.py:58 +#: indico_vc_zoom/plugin.py:59 msgid "Specify Zoom's webhook token if you want live updates" msgstr "" -#: indico_vc_zoom/plugin.py:60 +#: indico_vc_zoom/plugin.py:61 msgid "User lookup mode" msgstr "" -#: indico_vc_zoom/plugin.py:61 +#: indico_vc_zoom/plugin.py:62 msgid "" "Specify how Indico should look up the zoom user that corresponds to an " "Indico user." msgstr "" -#: indico_vc_zoom/plugin.py:64 +#: indico_vc_zoom/plugin.py:65 msgid "E-mail domains" msgstr "" -#: indico_vc_zoom/plugin.py:66 +#: indico_vc_zoom/plugin.py:67 msgid "" "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." msgstr "" -#: indico_vc_zoom/plugin.py:70 +#: indico_vc_zoom/plugin.py:71 msgid "Indico identity providers" msgstr "" -#: indico_vc_zoom/plugin.py:72 +#: indico_vc_zoom/plugin.py:73 msgid "" "Identity providers from which to get usernames. Indico queries those " "providers using the email addresses of the user and attempts to find Zoom" @@ -184,87 +184,91 @@ msgid "" "domain." msgstr "" -#: indico_vc_zoom/plugin.py:77 +#: indico_vc_zoom/plugin.py:78 msgid "Enterprise domain" msgstr "" -#: indico_vc_zoom/plugin.py:79 +#: indico_vc_zoom/plugin.py:80 msgid "" "The domain name used together with the usernames from the Indico identity" " provider" msgstr "" -#: indico_vc_zoom/plugin.py:82 +#: indico_vc_zoom/plugin.py:83 msgid "Allow Webinars (Experimental)" msgstr "" -#: indico_vc_zoom/plugin.py:84 +#: indico_vc_zoom/plugin.py:85 msgid "Allow webinars to be created through Indico. Use at your own risk." msgstr "" -#: indico_vc_zoom/plugin.py:98 +#: indico_vc_zoom/plugin.py:99 msgid "Join Before Host" msgstr "" -#: indico_vc_zoom/plugin.py:100 +#: indico_vc_zoom/plugin.py:101 msgid "" "Allow participants to join the meeting before the host starts the " "meeting. Only used for scheduled or recurring meetings." msgstr "" -#: indico_vc_zoom/plugin.py:107 +#: indico_vc_zoom/plugin.py:108 msgid "Creation email footer" msgstr "" -#: indico_vc_zoom/plugin.py:108 +#: indico_vc_zoom/plugin.py:109 msgid "Footer to append to emails sent upon creation of a VC room" msgstr "" -#: indico_vc_zoom/plugin.py:110 +#: indico_vc_zoom/plugin.py:111 msgid "Send host URL" msgstr "" -#: indico_vc_zoom/plugin.py:112 +#: indico_vc_zoom/plugin.py:113 msgid "" "Whether to send an e-mail with the Host URL to the meeting host upon " "creation of a meeting" msgstr "" -#: indico_vc_zoom/plugin.py:118 +#: indico_vc_zoom/plugin.py:119 msgid "Invalid identity providers: {}" msgstr "" -#: indico_vc_zoom/plugin.py:319 +#: indico_vc_zoom/plugin.py:335 msgid "" "Could not create the room in Zoom. Please contact support if the error " "persists" msgstr "" -#: indico_vc_zoom/plugin.py:403 +#: indico_vc_zoom/plugin.py:419 msgid "Room didn't existing in Zoom anymore" msgstr "" -#: indico_vc_zoom/plugin.py:406 +#: indico_vc_zoom/plugin.py:422 msgid "Zoom Error: \"{}\"" msgstr "" -#: indico_vc_zoom/plugin.py:409 +#: indico_vc_zoom/plugin.py:425 msgid "Problem deleting room" msgstr "" -#: indico_vc_zoom/plugin.py:490 +#: indico_vc_zoom/plugin.py:449 +msgid "The room \"{}\" no longer exists in Zoom and was removed from the event" +msgstr "" + +#: indico_vc_zoom/plugin.py:517 msgid "" "There are one or more scheduled Zoom meetings associated with this event " "which were not automatically updated." msgstr "" -#: indico_vc_zoom/plugin.py:493 +#: indico_vc_zoom/plugin.py:520 msgid "" "There are one or more scheduled Zoom meetings associated with the " "contribution \"{}\" which were not automatically updated." msgstr "" -#: indico_vc_zoom/plugin.py:496 +#: indico_vc_zoom/plugin.py:523 msgid "" "There are one or more scheduled Zoom meetings associated with this " "session block which were not automatically updated." @@ -292,11 +296,15 @@ msgid "" "persists." msgstr "" -#: indico_vc_zoom/util.py:150 +#: indico_vc_zoom/util.py:153 +msgid "Room no longer exists in Zoom" +msgstr "" + +#: indico_vc_zoom/util.py:155 msgid "Can't update meeting. Please contact support if the error persists." msgstr "" -#: indico_vc_zoom/util.py:206 +#: indico_vc_zoom/util.py:211 msgid "Could not find Zoom user for alternative host" msgstr "" diff --git a/vc_zoom/indico_vc_zoom/util.py b/vc_zoom/indico_vc_zoom/util.py index 08e8bb7..69bda58 100644 --- a/vc_zoom/indico_vc_zoom/util.py +++ b/vc_zoom/indico_vc_zoom/util.py @@ -145,6 +145,11 @@ def update_zoom_meeting(zoom_id, changes, is_webinar=False): 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."))