VC/Zoom: Initial Commit

This commit is contained in:
gmariano 2020-04-04 08:11:09 +02:00 committed by Pedro Ferreira
commit dc91b69c0b
37 changed files with 32192 additions and 0 deletions

84
.gitignore vendored Executable file
View File

@ -0,0 +1,84 @@
.vscode/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
venv/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/build/
docs/source/generated/
# pytest
.pytest_cache/
# PyBuilder
target/
# Editor files
#mac
.DS_Store
*~
#vim
*.swp
*.swo
#pycharm
.idea/*
#Ipython Notebook
.ipynb_checkpoints
webpack-build-config.json

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
graft indico_vc_zoom/static
graft indico_vc_zoom/migrations
graft indico_vc_zoom/templates
graft indico_vc_zoom/translations
global-exclude *.pyc __pycache__ .keep

7
README.md Normal file
View File

@ -0,0 +1,7 @@
Indico Plugin for Zoom based on Vidyo plugin.
To obtain Api key and api secret, please visit [https://marketplace.zoom.us/docs/guides/auth/jwt](https://marketplace.zoom.us/docs/guides/auth/jwt)
Not ready for production.
Developed by Giovanni Mariano @ ENEA Frascati

View File

@ -0,0 +1,12 @@
from __future__ import unicode_literals
from indico.core import signals
from indico.util.i18n import make_bound_gettext
_ = make_bound_gettext('vc_zoom')
@signals.import_tasks.connect
def _import_tasks(sender, **kwargs):
import indico_vc_zoom.task

View File

@ -0,0 +1,7 @@
from .client import ZoomIndicoClient, APIException, RoomNotFoundAPIException, ZoomClient
__all__ = ['ZoomIndicoClient', 'APIException', 'RoomNotFoundAPIException', 'ZoomClient']

View File

@ -0,0 +1,487 @@
from __future__ import absolute_import, unicode_literals
from requests import Session
from requests.auth import HTTPBasicAuth
import contextlib
import json
import requests
import time
import jwt
class ApiClient(object):
"""Simple wrapper for REST API requests"""
def __init__(self, base_uri=None, timeout=15, **kwargs):
"""Setup a new API Client
:param base_uri: The base URI to the API
:param timeout: The timeout to use for requests
:param kwargs: Any other attributes. These will be added as
attributes to the ApiClient object.
"""
self.base_uri = base_uri
self.timeout = timeout
for k, v in kwargs.items():
setattr(self, k, v)
@property
def timeout(self):
"""The timeout"""
return self._timeout
@timeout.setter
def timeout(self, value):
"""The default timeout"""
if value is not None:
try:
value = int(value)
except ValueError:
raise ValueError("timeout value must be an integer")
self._timeout = value
@property
def base_uri(self):
"""The base_uri"""
return self._base_uri
@base_uri.setter
def base_uri(self, value):
"""The default base_uri"""
if value and value.endswith("/"):
value = value[:-1]
self._base_uri = value
def url_for(self, endpoint):
"""Get the URL for the given endpoint
:param endpoint: The endpoint
:return: The full URL for the endpoint
"""
if not endpoint.startswith("/"):
endpoint = "/{}".format(endpoint)
if endpoint.endswith("/"):
endpoint = endpoint[:-1]
return self.base_uri + endpoint
def get_request(self, endpoint, params=None, headers=None):
"""Helper function for GET requests
:param endpoint: The endpoint
:param params: The URL parameters
:param headers: request headers
:return: The :class:``requests.Response`` object for this request
"""
if headers is None:
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
return requests.get(
self.url_for(endpoint), params=params, headers=headers, timeout=self.timeout
)
def post_request(
self, endpoint, params=None, data=None, headers=None, cookies=None
):
"""Helper function for POST requests
:param endpoint: The endpoint
:param params: The URL parameters
:param data: The data (either as a dict or dumped JSON string) to
include with the POST
:param headers: request headers
:param cookies: request cookies
:return: The :class:``requests.Response`` object for this request
"""
if data and not is_str_type(data):
data = json.dumps(data)
if headers is None:
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
return requests.post(
self.url_for(endpoint),
params=params,
data=data,
headers=headers,
cookies=cookies,
timeout=self.timeout,
)
def patch_request(
self, endpoint, params=None, data=None, headers=None, cookies=None
):
"""Helper function for PATCH requests
:param endpoint: The endpoint
:param params: The URL parameters
:param data: The data (either as a dict or dumped JSON string) to
include with the PATCH
:param headers: request headers
:param cookies: request cookies
:return: The :class:``requests.Response`` object for this request
"""
if data and not is_str_type(data):
data = json.dumps(data)
if headers is None:
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
return requests.patch(
self.url_for(endpoint),
params=params,
data=data,
headers=headers,
cookies=cookies,
timeout=self.timeout,
)
def delete_request(
self, endpoint, params=None, data=None, headers=None, cookies=None
):
"""Helper function for DELETE requests
:param endpoint: The endpoint
:param params: The URL parameters
:param data: The data (either as a dict or dumped JSON string) to
include with the DELETE
:param headers: request headers
:param cookies: request cookies
:return: The :class:``requests.Response`` object for this request
"""
if data and not is_str_type(data):
data = json.dumps(data)
if headers is None:
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
return requests.delete(
self.url_for(endpoint),
params=params,
data=data,
headers=headers,
cookies=cookies,
timeout=self.timeout,
)
def put_request(self, endpoint, params=None, data=None, headers=None, cookies=None):
"""Helper function for PUT requests
:param endpoint: The endpoint
:param params: The URL paramaters
:param data: The data (either as a dict or dumped JSON string) to
include with the PUT
:param headers: request headers
:param cookies: request cookies
:return: The :class:``requests.Response`` object for this request
"""
if data and not is_str_type(data):
data = json.dumps(data)
if headers is None:
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
return requests.put(
self.url_for(endpoint),
params=params,
data=data,
headers=headers,
cookies=cookies,
timeout=self.timeout,
)
@contextlib.contextmanager
def ignored(*exceptions):
"""Simple context manager to ignore expected Exceptions
:param \*exceptions: The exceptions to safely ignore
"""
try:
yield
except exceptions:
pass
def is_str_type(val):
"""Check whether the input is of a string type.
We use this method to ensure python 2-3 capatibility.
:param val: The value to check wither it is a string
:return: In python2 it will return ``True`` if :attr:`val` is either an
instance of str or unicode. In python3 it will return ``True`` if
it is an instance of str
"""
with ignored(NameError):
return isinstance(val, basestring)
return isinstance(val, str)
def require_keys(d, keys, allow_none=True):
"""Require that the object have the given keys
:param d: The dict the check
:param keys: The keys to check :attr:`obj` for. This can either be a single
string, or an iterable of strings
:param allow_none: Whether ``None`` values are allowed
:raises:
:ValueError: If any of the keys are missing from the obj
"""
if is_str_type(keys):
keys = [keys]
for k in keys:
if k not in d:
raise ValueError("'{}' must be set".format(k))
if not allow_none and d[k] is None:
raise ValueError("'{}' cannot be None".format(k))
return True
def date_to_str_gmt(d):
"""Convert date and datetime objects to a string
Note, this does not do any timezone conversion.
:param d: The :class:`datetime.date` or :class:`datetime.datetime` to
convert to a string
:returns: The string representation of the date
"""
return d.strftime("%Y-%m-%dT%H:%M:%SZ")
def date_to_str_local(d):
"""Convert date and datetime objects to a string
Note, this does not do any timezone conversion.
:param d: The :class:`datetime.date` or :class:`datetime.datetime` to
convert to a string
:returns: The string representation of the date
"""
return d.strftime("%Y-%m-%dT%H:%M:%S")
def generate_jwt(key, secret):
header = {"alg": "HS256", "typ": "JWT"}
payload = {"iss": key, "exp": int(time.time() + 3600)}
token = jwt.encode(payload, secret, algorithm="HS256", headers=header)
return token.decode("utf-8")
class APIException(Exception):
pass
class RoomNotFoundAPIException(APIException):
pass
class BaseComponent(ApiClient):
"""A base component"""
def __init__(self, base_uri=None, config=None, timeout=15, **kwargs):
"""Setup a base component
:param base_uri: The base URI to the API
:param config: The config details
:param timeout: The timeout to use for requests
:param kwargs: Any other attributes. These will be added as
attributes to the ApiClient object.
"""
super(BaseComponent, self).__init__(
base_uri=base_uri, timeout=timeout, config=config, **kwargs
)
def post_request(
self, endpoint, params=None, data=None, headers=None, cookies=None
):
"""Helper function for POST requests
Since the Zoom.us API only uses POST requests and each post request
must include all of the config data, this method ensures that all
of that data is there
:param endpoint: The endpoint
:param params: The URL parameters
:param data: The data (either as a dict or dumped JSON string) to
include with the POST
:param headers: request headers
:param cookies: request cookies
:return: The :class:``requests.Response`` object for this request
"""
params = params or {}
if headers is None:
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
return super(BaseComponent, self).post_request(
endpoint, params=params, data=data, headers=headers, cookies=cookies
)
class MeetingComponent(BaseComponent):
def list(self, **kwargs):
require_keys(kwargs, "user_id")
return self.get_request(
"/users/{}/meetings".format(kwargs.get("user_id")), params=kwargs
)
def create(self, **kwargs):
require_keys(kwargs, "user_id")
if kwargs.get("start_time"):
if kwargs.get("timezone"):
kwargs["start_time"] = date_to_str_local(kwargs["start_time"])
else:
kwargs["start_time"] = date_to_str_gmt(kwargs["start_time"])
return self.post_request(
"/users/{}/meetings".format(kwargs.get("user_id")), params=kwargs['user_id'], data={x: kwargs[x] for x in kwargs.keys() if x not in ['user_id']}
)
def get(self, **kwargs):
require_keys(kwargs, "id")
return self.get_request("/meetings/{}".format(kwargs.get("id")), params=kwargs)
def update(self, **kwargs):
require_keys(kwargs, "id")
if kwargs.get("start_time"):
if kwargs.get("timezone"):
kwargs["start_time"] = date_to_str_local(kwargs["start_time"])
else:
kwargs["start_time"] = date_to_str_gmt(kwargs["start_time"])
return self.patch_request(
"/meetings/{}".format(kwargs.get("id")), params=kwargs
)
def delete(self, **kwargs):
require_keys(kwargs, "id")
return self.delete_request(
"/meetings/{}".format(kwargs.get("id")), params=kwargs
)
def get_invitation(self, **kwargs):
require_keys(kwargs, "id")
return self.get_request("/meetings/{}/invitation".format(kwargs.get("id")), params=kwargs)
class UserComponent(BaseComponent):
def list(self, **kwargs):
return self.get_request("/users", params=kwargs)
def create(self, **kwargs):
return self.post_request("/users", params=kwargs)
def update(self, **kwargs):
require_keys(kwargs, "id")
return self.patch_request("/users/{}".format(kwargs.get("id")), params=kwargs)
def delete(self, **kwargs):
require_keys(kwargs, "id")
return self.delete_request("/users/{}".format(kwargs.get("id")), params=kwargs)
def get(self, **kwargs):
require_keys(kwargs, "id")
return self.get_request("/users/{}".format(kwargs.get("id")), params=kwargs)
class ZoomClient(ApiClient):
"""Zoom.us REST API Python Client"""
"""Base URL for Zoom API"""
def __init__(
self, api_key, api_secret, data_type="json", timeout=15
):
"""Create a new Zoom client
:param api_key: The Zooom.us API key
:param api_secret: The Zoom.us API secret
:param data_type: The expected return data type. Either 'json' or 'xml'
:param timeout: The time out to use for API requests
"""
BASE_URI = "https://api.zoom.us/v2"
self.components = {"user": UserComponent,
"meeting": MeetingComponent}
super(ZoomClient, self).__init__(base_uri=BASE_URI, timeout=timeout)
# Setup the config details
self.config = {
"api_key": api_key,
"api_secret": api_secret,
"data_type": data_type,
"token": generate_jwt(api_key, api_secret),
}
# Instantiate the components
for key in self.components.keys():
self.components[key] = self.components[key](
base_uri=BASE_URI, config=self.config
)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return
def refresh_token(self):
self.config["token"] = (
generate_jwt(self.config["api_key"], self.config["api_secret"]),
)
@property
def api_key(self):
"""The Zoom.us api_key"""
return self.config.get("api_key")
@api_key.setter
def api_key(self, value):
"""Set the api_key"""
self.config["api_key"] = value
self.refresh_token()
@property
def api_secret(self):
"""The Zoom.us api_secret"""
return self.config.get("api_secret")
@api_secret.setter
def api_secret(self, value):
"""Set the api_secret"""
self.config["api_secret"] = value
self.refresh_token()
@property
def meeting(self):
"""Get the meeting component"""
return self.components.get("meeting")
@property
def user(self):
"""Get the user component"""
return self.components.get("user")
class ZoomIndicoClient(object):
def __init__(self, settings):
api_key=settings.get('api_key')
api_secret=settings.get('api_secret')
self.client=ZoomClient(api_key, api_secret)
def create_meeting(self, **kwargs):
return json.loads(self.client.meeting.create(**kwargs).content)
def get_meeting(self, zoom_id):
return json.loads(self.client.meeting.get(id=zoom_id).content)
def get_meeting_invitation(self, zoom_id):
return json.loads(self.client.meeting.get_invitation(id=zoom_id).content)
def delete_meeting(self, zoom_id):
self.client.meeting.delete(id=zoom_id)
def check_user_meeting_time(self, userID, start_dt, end_dt):
pass

View File

@ -0,0 +1,16 @@
from __future__ import unicode_literals
from indico.core.plugins import IndicoPluginBlueprint
from indico_vc_zoom.controllers import RHZoomRoomOwner
blueprint = IndicoPluginBlueprint('vc_zoom', 'indico_vc_zoom')
# Room management
# using any(zoom) instead of defaults since the event vc room locator
# includes the service and normalization skips values provided in 'defaults'
blueprint.add_url_rule('/event/<confId>/manage/videoconference/<any(zoom):service>/<int:event_vc_room_id>/room-owner',
'set_room_owner', RHZoomRoomOwner, methods=('POST',))

35
indico_vc_zoom/cli.py Normal file
View File

@ -0,0 +1,35 @@
from __future__ import unicode_literals
import click
from terminaltables import AsciiTable
from indico.cli.core import cli_group
from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomStatus
@cli_group(name='zoom')
def cli():
"""Manage the Zoom plugin."""
@cli.command()
@click.option('--status', type=click.Choice(['deleted', 'created']))
def rooms(status=None):
"""Lists all Zoom rooms"""
room_query = VCRoom.find(type='zoom')
table_data = [['ID', 'Name', 'Status', 'Zoom ID', 'Meeting']]
if status:
room_query = room_query.filter(VCRoom.status == VCRoomStatus.get(status))
for room in room_query:
table_data.append([unicode(room.id), room.name, room.status.name,
unicode(room.data['zoom_id']), unicode(room.zoom_meeting.meeting)])
table = AsciiTable(table_data)
for col in (0, 3, 4):
table.justify_columns[col] = 'right'
print table.table

View File

@ -0,0 +1,34 @@
// This file is part of the Indico plugins.
// Copyright (C) 2002 - 2019 CERN
//
// The Indico plugins are free software; you can redistribute
// them and/or modify them under the terms of the MIT License;
// see the LICENSE file for more details.
$(function() {
$('.vc-toolbar').dropdown({
positioning: {
level1: {my: 'right top', at: 'right bottom', offset: '0px 0px'},
},
});
$('.vc-toolbar .action-make-owner').click(function() {
const $this = $(this);
$.ajax({
url: $this.data('href'),
method: 'POST',
complete: IndicoUI.Dialogs.Util.progress(),
})
.done(function(result) {
if (handleAjaxError(result)) {
return;
} else {
location.reload();
}
})
.fail(function(error) {
handleAjaxError(error);
});
});
});

View File

@ -0,0 +1,26 @@
from __future__ import unicode_literals
from flask import flash, jsonify, session
from indico.core.db import db
from indico.modules.vc.controllers import RHVCSystemEventBase
from indico.modules.vc.exceptions import VCRoomError
from indico.util.i18n import _
class RHZoomRoomOwner(RHVCSystemEventBase):
def _process(self):
result = {}
self.vc_room.zoom_meeting.owned_by_user = session.user
try:
self.plugin.update_room(self.vc_room, self.event)
except VCRoomError as err:
result['error'] = {'message': err.message}
result['success'] = False
db.session.rollback()
else:
flash(_("You are now the owner of the room '{room.name}'".format(room=self.vc_room)), 'success')
result['success'] = True
return jsonify(result)

67
indico_vc_zoom/forms.py Normal file
View File

@ -0,0 +1,67 @@
from __future__ import unicode_literals
from flask import session
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"))
class VCRoomAttachForm(VCRoomAttachFormBase, ZoomAdvancedFormMixin):
pass
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."))
#if not next(iter_user_identities(field.data), None):
# raise ValidationError(_("This user does not have a suitable account to use Zoom."))

View File

@ -0,0 +1,51 @@
from flask import request
from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomStatus
from indico.web.http_api.hooks.base import HTTPAPIHook
class DeleteVCRoomAPI(HTTPAPIHook):
PREFIX = 'api'
TYPES = ('deletevcroom',)
RE = r'zoom'
GUEST_ALLOWED = False
VALID_FORMATS = ('json',)
COMMIT = True
HTTP_POST = True
def _has_access(self, user):
from indico_vc_zoom.plugin import ZoomPlugin
return user in ZoomPlugin.settings.acls.get('managers')
def _getParams(self):
super(DeleteVCRoomAPI, self)._getParams()
self._room_ids = map(int, request.form.getlist('rid'))
def api_deletevcroom(self, user):
from indico_vc_zoom.plugin import ZoomPlugin
from indico_vc_zoom.api import APIException
success = []
failed = []
not_in_db = []
for rid in self._room_ids:
room = VCRoom.query.filter(VCRoom.type == 'zoom',
VCRoom.status == VCRoomStatus.created,
VCRoom.data.contains({'zoom_id': str(rid)})).first()
if not room:
not_in_db.append(rid)
continue
try:
room.plugin.delete_meeting(room, None)
except APIException:
failed.append(rid)
ZoomPlugin.logger.exception('Could not delete VC room %s', room)
else:
room.status = VCRoomStatus.deleted
success.append(rid)
ZoomPlugin.logger.info('%s deleted', room)
return {'success': success, 'failed': failed, 'missing': not_in_db}

View File

@ -0,0 +1,52 @@
"""crea tabella
Revision ID: b79318836818
Revises:
Create Date: 2020-03-12 10:30:09.256157
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql.ddl import CreateSchema, DropSchema
# revision identifiers, used by Alembic.
revision = 'b79318836818'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
op.execute(CreateSchema('plugin_vc_zoom'))
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('zoom_extensions',
sa.Column('vc_room_id', sa.Integer(), nullable=False),
sa.Column('url_zoom', sa.Text(), nullable=False),
sa.ForeignKeyConstraint(['vc_room_id'], [u'events.vc_rooms.id'], name=op.f('fk_zoom_extensions_vc_room_id_vc_rooms')),
sa.PrimaryKeyConstraint('vc_room_id', name=op.f('pk_zoom_extensions')),
schema='plugin_vc_zoom'
)
op.create_index(op.f('ix_zoom_extensions_url_zoom'), 'zoom_extensions', ['url_zoom'], unique=False, schema='plugin_vc_zoom')
op.create_table('zoom_licenses',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('license_id', sa.Text(), nullable=False),
sa.Column('license_name', sa.Text(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_zoom_licenses')),
schema='plugin_vc_zoom'
)
op.create_index(op.f('ix_uq_zoom_licenses_license_id'), 'zoom_licenses', ['license_id'], unique=True, schema='plugin_vc_zoom')
op.create_index(op.f('ix_zoom_licenses_license_name'), 'zoom_licenses', ['license_name'], unique=False, schema='plugin_vc_zoom')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_zoom_licenses_license_name'), table_name='zoom_licenses', schema='plugin_vc_zoom')
op.drop_index(op.f('ix_uq_zoom_licenses_license_id'), table_name='zoom_licenses', schema='plugin_vc_zoom')
op.drop_table('zoom_licenses', schema='plugin_vc_zoom')
op.drop_index(op.f('ix_zoom_extensions_url_zoom'), table_name='zoom_extensions', schema='plugin_vc_zoom')
op.drop_table('zoom_extensions', schema='plugin_vc_zoom')
# ### end Alembic commands ###
op.execute(DropSchema('plugin_vc_zoom'))

View File

@ -0,0 +1,66 @@
"""create table
Revision ID: d86e85a383c1
Revises: b79318836818
Create Date: 2020-04-03 19:57:35.771416
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'd86e85a383c1'
down_revision = 'b79318836818'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('zoom_meetings',
sa.Column('vc_room_id', sa.Integer(), nullable=False),
sa.Column('meeting', sa.BigInteger(), nullable=True),
sa.Column('url_zoom', sa.Text(), nullable=False),
sa.Column('owned_by_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['owned_by_id'], [u'users.users.id'], name=op.f('fk_zoom_meetings_owned_by_id_users')),
sa.ForeignKeyConstraint(['vc_room_id'], [u'events.vc_rooms.id'], name=op.f('fk_zoom_meetings_vc_room_id_vc_rooms')),
sa.PrimaryKeyConstraint('vc_room_id', name=op.f('pk_zoom_meetings')),
schema='plugin_vc_zoom'
)
op.create_index(op.f('ix_zoom_meetings_meeting'), 'zoom_meetings', ['meeting'], unique=False, schema='plugin_vc_zoom')
op.create_index(op.f('ix_zoom_meetings_owned_by_id'), 'zoom_meetings', ['owned_by_id'], unique=False, schema='plugin_vc_zoom')
op.create_index(op.f('ix_zoom_meetings_url_zoom'), 'zoom_meetings', ['url_zoom'], unique=False, schema='plugin_vc_zoom')
op.drop_index('ix_zoom_extensions_url_zoom', table_name='zoom_extensions', schema='plugin_vc_zoom')
op.drop_table('zoom_extensions', schema='plugin_vc_zoom')
op.drop_index('ix_uq_zoom_licenses_license_id', table_name='zoom_licenses', schema='plugin_vc_zoom')
op.drop_index('ix_zoom_licenses_license_name', table_name='zoom_licenses', schema='plugin_vc_zoom')
op.drop_table('zoom_licenses', schema='plugin_vc_zoom')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('zoom_licenses',
sa.Column('id', sa.INTEGER(), server_default=sa.text(u"nextval('plugin_vc_zoom.zoom_licenses_id_seq'::regclass)"), autoincrement=True, nullable=False),
sa.Column('license_id', sa.TEXT(), autoincrement=False, nullable=False),
sa.Column('license_name', sa.TEXT(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('id', name=u'pk_zoom_licenses'),
schema='plugin_vc_zoom'
)
op.create_index('ix_zoom_licenses_license_name', 'zoom_licenses', ['license_name'], unique=False, schema='plugin_vc_zoom')
op.create_index('ix_uq_zoom_licenses_license_id', 'zoom_licenses', ['license_id'], unique=True, schema='plugin_vc_zoom')
op.create_table('zoom_extensions',
sa.Column('vc_room_id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column('url_zoom', sa.TEXT(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['vc_room_id'], [u'events.vc_rooms.id'], name=u'fk_zoom_extensions_vc_room_id_vc_rooms'),
sa.PrimaryKeyConstraint('vc_room_id', name=u'pk_zoom_extensions'),
schema='plugin_vc_zoom'
)
op.create_index('ix_zoom_extensions_url_zoom', 'zoom_extensions', ['url_zoom'], unique=False, schema='plugin_vc_zoom')
op.drop_index(op.f('ix_zoom_meetings_url_zoom'), table_name='zoom_meetings', schema='plugin_vc_zoom')
op.drop_index(op.f('ix_zoom_meetings_owned_by_id'), table_name='zoom_meetings', schema='plugin_vc_zoom')
op.drop_index(op.f('ix_zoom_meetings_meeting'), table_name='zoom_meetings', schema='plugin_vc_zoom')
op.drop_table('zoom_meetings', schema='plugin_vc_zoom')
# ### end Alembic commands ###

View File

@ -0,0 +1,46 @@
"""create table
Revision ID: d54585a383v1
Revises: b79318836818
Create Date: 2020-04-05 15:39:35.771416
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql.ddl import CreateSchema, DropSchema
# revision identifiers, used by Alembic.
revision = 'd54585a383v1'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.execute(CreateSchema('plugin_vc_zoom'))
op.create_table('zoom_meetings',
sa.Column('vc_room_id', sa.Integer(), nullable=False),
sa.Column('meeting', sa.BigInteger(), nullable=True),
sa.Column('url_zoom', sa.Text(), nullable=False),
sa.Column('owned_by_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['owned_by_id'], [u'users.users.id'], name=op.f('fk_zoom_meetings_owned_by_id_users')),
sa.ForeignKeyConstraint(['vc_room_id'], [u'events.vc_rooms.id'], name=op.f('fk_zoom_meetings_vc_room_id_vc_rooms')),
sa.PrimaryKeyConstraint('vc_room_id', name=op.f('pk_zoom_meetings')),
schema='plugin_vc_zoom'
)
op.create_index(op.f('ix_zoom_meetings_meeting'), 'zoom_meetings', ['meeting'], unique=False, schema='plugin_vc_zoom')
op.create_index(op.f('ix_zoom_meetings_owned_by_id'), 'zoom_meetings', ['owned_by_id'], unique=False, schema='plugin_vc_zoom')
op.create_index(op.f('ix_zoom_meetings_url_zoom'), 'zoom_meetings', ['url_zoom'], unique=False, schema='plugin_vc_zoom')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_zoom_meetings_url_zoom'), table_name='zoom_meetings', schema='plugin_vc_zoom')
op.drop_index(op.f('ix_zoom_meetings_owned_by_id'), table_name='zoom_meetings', schema='plugin_vc_zoom')
op.drop_index(op.f('ix_zoom_meetings_meeting'), table_name='zoom_meetings', schema='plugin_vc_zoom')
op.drop_table('zoom_meetings', schema='plugin_vc_zoom')
op.execute(DropSchema('plugin_vc_zoom'))
# ### end Alembic commands ###

View File

View File

@ -0,0 +1,75 @@
from __future__ import unicode_literals
import urllib
from sqlalchemy.event import listens_for
from sqlalchemy.orm.attributes import flag_modified
from indico.core.db.sqlalchemy import db
from indico.util.string import return_ascii
class ZoomMeeting(db.Model):
__tablename__ = 'zoom_meetings'
__table_args__ = {'schema': 'plugin_vc_zoom'}
#: ID of the videoconference room
vc_room_id = db.Column(
db.Integer,
db.ForeignKey('events.vc_rooms.id'),
primary_key=True
)
meeting = db.Column(
db.BigInteger,
index=True
)
url_zoom = db.Column(
db.Text,
index=True,
nullable=False
)
owned_by_id = db.Column(
db.Integer,
db.ForeignKey('users.users.id'),
index=True,
nullable=False
)
vc_room = db.relationship(
'VCRoom',
lazy=False,
backref=db.backref(
'zoom_meeting',
cascade='all, delete-orphan',
uselist=False,
lazy=False
)
)
#: The user who owns the Zoom room
owned_by_user = db.relationship(
'User',
lazy=True,
backref=db.backref(
'vc_rooms_zoom',
lazy='dynamic'
)
)
@property
def join_url(self):
from indico_vc_zoom.plugin import ZoomPlugin
url = self.vc_room.data['url']
return url
@return_ascii
def __repr__(self):
return '<ZoomMeeting({}, {}, {})>'.format(self.vc_room, self.meeting, self.owned_by_user)
@listens_for(ZoomMeeting.owned_by_user, 'set')
def _owned_by_user_set(target, user, *unused):
if target.vc_room and user.as_principal != tuple(target.vc_room.data['owner']):
target.vc_room.data['owner'] = user.as_principal
flag_modified(target.vc_room, 'data')

260
indico_vc_zoom/plugin.py Normal file
View File

@ -0,0 +1,260 @@
from __future__ import unicode_literals
from flask import session
from flask_pluginengine import current_plugin
from sqlalchemy.orm.attributes import flag_modified
from wtforms.fields.core import BooleanField
from wtforms.fields import IntegerField, TextAreaField
from wtforms.fields.html5 import EmailField, URLField
from wtforms.fields.simple import StringField, BooleanField
from indico.web.forms.widgets import SwitchWidget
from indico.web.flask.templating import get_template_module
from wtforms.validators import DataRequired, NumberRange
from indico.core.notifications import make_email, send_email
from indico.core.plugins import get_plugin_template_module
from indico.core import signals
from indico.core.auth import multipass
from indico.core.config import config
from indico.core.plugins import IndicoPlugin, 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.views import WPVCEventPage, WPVCManageEvent
from indico.web.forms.fields import IndicoPasswordField
from indico.web.forms.widgets import CKEditorWidget
from indico.web.http_api.hooks.base import HTTPAPIHook
from indico.modules.vc.notifications import _send
from indico_vc_zoom import _
from indico_vc_zoom.api import ZoomIndicoClient, APIException, RoomNotFoundAPIException
from indico_vc_zoom.blueprint import blueprint
from indico_vc_zoom.cli import cli
from indico_vc_zoom.forms import VCRoomAttachForm, VCRoomForm
from indico_vc_zoom.http_api import DeleteVCRoomAPI
from indico_vc_zoom.models.zoom_meetings import ZoomMeeting
from indico_vc_zoom.util import iter_extensions, iter_user_identities, retrieve_principal, update_room_from_obj
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'))
class ZoomPlugin(VCPluginMixin, IndicoPlugin):
"""Zoom
Videoconferencing with Zoom
"""
configurable = True
settings_form = PluginSettingsForm
vc_room_form = VCRoomForm
vc_room_attach_form = VCRoomAttachForm
friendly_name = 'Zoom'
def init(self):
super(ZoomPlugin, self).init()
self.connect(signals.plugin.cli, self._extend_indico_cli)
self.inject_bundle('main.js', WPSimpleEventDisplay)
self.inject_bundle('main.js', WPVCEventPage)
self.inject_bundle('main.js', WPVCManageEvent)
HTTPAPIHook.register(DeleteVCRoomAPI)
@property
def default_settings(self):
return dict(VCPluginMixin.default_settings, **{
'support_email': config.SUPPORT_EMAIL,
'api_key': '',
'api_secret': '',
#'indico_room_prefix': 10,
'auto_mute':True,
'host_video':False,
'participant_video':True,
'join_before_host':True,
#'room_group_name': 'Indico',
# we skip identity providers in the default list if they don't support get_identity.
# these providers (local accounts, oauth) are unlikely be the correct ones to integrate
# with the zoom infrastructure.
'num_days_old': 5,
'max_rooms_warning': 5000,
'zoom_phone_link': None,
'creation_email_footer': None
})
@property
def logo_url(self):
return url_for_plugin(self.name + '.static', filename='images/zoom_logo.png')
@property
def icon_url(self):
return url_for_plugin(self.name + '.static', filename='images/zoom_logo.png')
def _extend_indico_cli(self, sender, **kwargs):
return cli
def update_data_association(self, event, vc_room, event_vc_room, data):
super(ZoomPlugin, self).update_data_association(event, vc_room, event_vc_room, data)
event_vc_room.data.update({key: data.pop(key) for key in [
'show_autojoin',
'show_phone_numbers'
]})
flag_modified(event_vc_room, 'data')
def update_data_vc_room(self, vc_room, data):
super(ZoomPlugin, self).update_data_vc_room(vc_room, data)
for key in ['description', 'owner', 'auto_mute', 'host_video', 'participant_video', 'join_before_host']:
if key in data:
vc_room.data[key] = data.pop(key)
flag_modified(vc_room, 'data')
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)
def update_room(self, vc_room, event):
pass
def refresh_room(self, vc_room, event):
pass
def delete_room(self, vc_room, event):
client = ZoomIndicoClient(self.settings)
zoom_id = vc_room.data['zoom_id']
client.delete_meeting(zoom_id)
def get_meeting(self, vc_room):
client = ZoomIndicoClient(self.settings)
return client.get_meeting(vc_room.data['zoom_id'])
def get_blueprints(self):
return blueprint
def get_vc_room_form_defaults(self, event):
defaults = super(ZoomPlugin, self).get_vc_room_form_defaults(event)
defaults.update({
'show_autojoin': True,
'show_phone_numbers': True,
'owner_user': session.user
})
return defaults
def get_vc_room_attach_form_defaults(self, event):
defaults = super(ZoomPlugin, self).get_vc_room_attach_form_defaults(event)
defaults.update({
'show_autojoin': True,
'show_phone_numbers': True
})
return defaults
def can_manage_vc_room(self, user, room):
return user == room.zoom_meeting.owned_by_user or super(ZoomPlugin, self).can_manage_vc_room(user, room)
def _merge_users(self, target, source, **kwargs):
super(ZoomPlugin, self)._merge_users(target, source, **kwargs)
for ext in ZoomMeeting.find(owned_by_user=source):
ext.owned_by_user = target
flag_modified(ext.vc_room, 'data')
def get_notification_cc_list(self, action, vc_room, event):
return {vc_room.zoom_meeting.owned_by_user.email}
def notify_owner_start_url(self, vc_room):
user = vc_room.zoom_meeting.owned_by_user
to_list = {user.email}
template_module = get_template_module('vc_zoom:emails/notify_start_url.html', plugin=ZoomPlugin.instance, vc_room=vc_room, event=None,
vc_room_event=None, user=user)
email = make_email(to_list, template=template_module, html=True)
send_email(email, None, 'Zoom')

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

70
indico_vc_zoom/task.py Normal file
View File

@ -0,0 +1,70 @@
from __future__ import unicode_literals
from datetime import timedelta
from celery.schedules import crontab
from indico.core.celery import celery
from indico.core.db import db
from indico.core.plugins import get_plugin_template_module
from indico.modules.events import Event
from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomEventAssociation, VCRoomStatus
from indico.modules.vc.notifications import _send
from indico.util.date_time import now_utc
from indico.util.struct.iterables import committing_iterator
from indico_vc_zoom.api import APIException, RoomNotFoundAPIException
def find_old_zoom_rooms(max_room_event_age):
"""Finds all Zoom rooms that are:
- linked to no events
- linked only to events whose start date precedes today - max_room_event_age days
"""
recently_used = (db.session.query(VCRoom.id)
.filter(VCRoom.type == 'zoom',
Event.end_dt > (now_utc() - timedelta(days=max_room_event_age)))
.join(VCRoom.events)
.join(VCRoomEventAssociation.event)
.group_by(VCRoom.id))
# non-deleted rooms with no recent associations
return VCRoom.find_all(VCRoom.status != VCRoomStatus.deleted, ~VCRoom.id.in_(recently_used))
def notify_owner(plugin, vc_room):
"""Notifies about the deletion of a Zoom room from the Zoom server."""
user = vc_room.zoom_meeting.owned_by_user
tpl = get_plugin_template_module('emails/remote_deleted.html', plugin=plugin, vc_room=vc_room, event=None,
vc_room_event=None, user=user)
_send('delete', user, plugin, None, vc_room, tpl)
@celery.periodic_task(run_every=crontab(minute='0', hour='3', day_of_week='monday'), plugin='vc_zoom')
def zoom_cleanup(dry_run=False):
from indico_vc_zoom.plugin import ZoomPlugin
max_room_event_age = ZoomPlugin.settings.get('num_days_old')
ZoomPlugin.logger.info('Deleting Zoom rooms that are not used or linked to events all older than %d days',
max_room_event_age)
candidate_rooms = find_old_zoom_rooms(max_room_event_age)
ZoomPlugin.logger.info('%d rooms found', len(candidate_rooms))
if dry_run:
for vc_room in candidate_rooms:
ZoomPlugin.logger.info('Would delete Zoom room %s from server', vc_room)
return
for vc_room in committing_iterator(candidate_rooms, n=20):
try:
ZoomPlugin.instance.delete_room(vc_room, None)
ZoomPlugin.logger.info('Room %s deleted from Zoom server', vc_room)
notify_owner(ZoomPlugin.instance, vc_room)
vc_room.status = VCRoomStatus.deleted
except RoomNotFoundAPIException:
ZoomPlugin.logger.warning('Room %s had been already deleted from the Zoom server', vc_room)
vc_room.status = VCRoomStatus.deleted
except APIException:
ZoomPlugin.logger.exception('Impossible to delete Zoom room %s', vc_room)

View File

@ -0,0 +1,6 @@
{% macro render_join_button(vc_room, extra_classes="") %}
<a class="i-button highlight {{ extra_classes }}"
href="{{ vc_room.zoom_meeting.join_url }}" target="_blank">
{% trans %}Join{% endtrans %}
</a>
{% endmacro %}

View File

@ -0,0 +1,16 @@
{% extends 'vc/emails/created.html' %}
{% block plugin_specific_info %}
<li>
<strong>Zoom URL</strong>:
<a href="{{ vc_room.zoom_meeting.join_url }}">{{ vc_room.zoom_meeting.join_url }}</a>
</li>
{% endblock %}
{% block custom_footer %}
{% if plugin.settings.get('creation_email_footer') %}
<hr>
{{ plugin.settings.get('creation_email_footer') | sanitize_html }}
{% endif %}
{% endblock %}

View File

@ -0,0 +1 @@
{% extends 'vc/emails/deleted.html' %}

View File

@ -0,0 +1,18 @@
{% extends 'emails/base.html' %}
{% block subject -%}
[DON'T SHARE] Zoom Host URL - {{ vc_room.name }}
{%- endblock %}
{% block header -%}
<li>
<strong> HOST Zoom URL</strong>:
<a href="{{ vc_room.data.start_url }}">{{ vc_room.data.start_url }}</a>
</li>
Please don't share this link.
{% block custom_footer %}{% endblock %}
{%- endblock %}

View File

@ -0,0 +1,16 @@
{% extends 'emails/base.html' %}
{% block subject -%}
[{{ plugin.friendly_name }}] Room deleted from server: {{ vc_room.name }}
{%- endblock %}
{% block header -%}
<p>
The Zoom room "{{ vc_room.name }}" has been deleted from the Zoom server since it has not been used by any recent event.
</p>
<p>
You won't be able to attach it to any future events. If you need to do so, please create a new room.
</p>
{% block custom_footer %}{% endblock %}
{%- endblock %}

View File

@ -0,0 +1,6 @@
{% extends 'vc/event_buttons.html' %}
{% from 'vc_zoom:buttons.html' import render_join_button %}
{% block buttons %}
{{ render_join_button(vc_room, "i-button-small event-service-right-button join-button") }}
{% endblock %}

View File

@ -0,0 +1,37 @@
{% from '_clipboard_input.html' import clipboard_input %}
{% set owner = vc_room.zoom_meeting.owned_by_user %}
{% set phone_link = settings.get('zoom_phone_link') %}
<dl>
<dt>{% trans %}Name{% endtrans %}</dt>
<dd>{{ vc_room.name }}</dd>
<dt>{% trans %}Meeting Topic{% endtrans %}</dt>
<dd>{{ vc_room.data.description }}</dd>
{% if vc_room.zoom_meeting %}
<dt>{% trans %}Meeting ID{% endtrans %}</dt>
<dd>{{ vc_room.zoom_meeting.meeting }}</dd>
{% endif %}
{% if owner %}
<dt>{% trans %}Owner{% endtrans %}</dt>
<dd>{{ owner.full_name }}</dd>
{% endif %}
{% if event_vc_room.data.show_pin and vc_room.data.room_pin %}
<dt>{% trans %}Room PIN{% endtrans %}</dt>
<dd>{{ vc_room.data.room_pin }}</dd>
{% endif %}
{% if event_vc_room.data.show_autojoin %}
<dt class="large-row">{% trans %}Zoom URL{% endtrans %}</dt>
<dd class="large-row">
{{ clipboard_input(vc_room.zoom_meeting.join_url, name="vc-room-url-%s"|format(event_vc_room.id)) }}
</dd>
{% endif %}
{% if event_vc_room.data.show_phone_numbers and phone_link %}
<dt>
{% trans %}Useful links{% endtrans %}
</dt>
<dd>
<a href="{{ phone_link }}" target="_blank">
{% trans %}Phone numbers{% endtrans %}
</a>
</dd>
{% endif %}
</dl>

View File

@ -0,0 +1,55 @@
{% from '_password.html' import password %}
{% from '_clipboard_input.html' import clipboard_input %}
{% set owner = vc_room.zoom_meeting.owned_by_user %}
{% set phone_link = settings.get('zoom_phone_link') %}
<dl class="details-container">
<dt>{% trans %}Meeting Topic{% endtrans %}</dt>
<dd>{{ vc_room.data.description }}</dd>
<dt>{% trans %}Meeting ID{% endtrans %}</dt>
<dd>{{ vc_room.zoom_meeting.meeting }}</dd>
<dt>{% trans %}Owner{% endtrans %}</dt>
<dd>
{% if owner %}
{{ owner.full_name }}
{% else %}
{{ vc_room.data.owner_account }} <em>(deleted)</em>
{% endif %}
</dd>
<dt>{% trans %}Linked to{% endtrans %}</dt>
<dd>
{% set obj = event_vc_room.link_object %}
{% if obj is none %}
<em>(missing {{ event_vc_room.link_type.name }})</em>
{% elif event_vc_room.link_type.name == 'event' %}
{% trans %}the whole event{% endtrans %}
{% elif event_vc_room.link_type.name == 'contribution' %}
{% trans %}Contribution{% endtrans %}: {{ obj.title }}
{% elif event_vc_room.link_type.name == 'block' %}
{% trans %}Session{% endtrans %}: {{ obj.full_title }}
{% endif %}
</dd>
{% if vc_room.data.room_pin %}
<dt>{% trans %}Room PIN{% endtrans %}</dt>
<dd>
{{ password('vc-room-pin-%s'|format(event_vc_room.id), value=vc_room.data.room_pin, toggle=True,
readonly=true) }}
</dd>
{% endif %}
{% if vc_room.data.moderation_pin %}
<dt>{% trans %}Moderation PIN{% endtrans %}</dt>
<dd>
{{ password('vc-moderation-pin-%s'|format(event_vc_room.id), value=vc_room.data.moderation_pin,
toggle=True, readonly=true) }}
</dd>
{% endif %}
<dt>{% trans %}Zoom URL{% endtrans %}</dt>
<dd>
{{ clipboard_input(vc_room.zoom_meeting.join_url, name="vc-room-url") }}
</dd>
<dt>{% trans %}Created on{% endtrans %}</dt>
<dd>{{ vc_room.created_dt | format_datetime(timezone=event.tzinfo) }}</dd>
{% if vc_room.modified_dt %}
<dt>{% trans %}Modified on{% endtrans %}</dt>
<dd>{{ vc_room.modified_dt | format_datetime(timezone=event.tzinfo) }}</dd>
{% endif %}
</dl>

View File

@ -0,0 +1,6 @@
{% extends 'vc/management_buttons.html' %}
{% from 'vc_zoom:buttons.html' import render_join_button %}
{% block buttons %}
{{ render_join_button(vc_room, extra_classes="icon-play") }}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'vc/vc_room_timetable_buttons.html' %}
{% from 'vc_zoom:buttons.html' import render_join_button %}
{% set vc_room = event_vc_room.vc_room %}
{% block buttons %}
{{ render_join_button(vc_room, "i-button-small event-service-right-button join-button") }}
{% endblock %}

View File

@ -0,0 +1,23 @@
# Translations template for PROJECT.
# Copyright (C) 2015 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
#
# Translators:
# Thomas Baron <thomas.baron@cern.ch>, 2015
msgid ""
msgstr ""
"Project-Id-Version: Indico\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2015-03-11 16:21+0100\n"
"PO-Revision-Date: 2015-03-12 12:52+0000\n"
"Last-Translator: Thomas Baron <thomas.baron@cern.ch>\n"
"Language-Team: French (France) (http://www.transifex.com/projects/p/indico/language/fr_FR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 1.3\n"
"Language: fr_FR\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Indico"
msgstr "Indico"

View File

@ -0,0 +1,289 @@
# Translations template for PROJECT.
# Copyright (C) 2017 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
#
# Translators:
# Thomas Baron <thomas.baron@cern.ch>, 2015,2017
msgid ""
msgstr ""
"Project-Id-Version: Indico\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2017-10-18 11:55+0200\n"
"PO-Revision-Date: 2017-10-30 11:04+0000\n"
"Last-Translator: Thomas Baron <thomas.baron@cern.ch>\n"
"Language-Team: French (France) (http://www.transifex.com/indico/indico/language/fr_FR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.5.1\n"
"Language: fr_FR\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: indico_vc_zoom/controllers.py:38
msgid "You are now the owner of the room '{room.name}'"
msgstr "Vous êtes maintenant responsable de la salle '{room.name}'"
#: indico_vc_zoom/forms.py:32
msgid "The PIN must be a number"
msgstr "Le code confidentiel doit être un nombre entier"
#: indico_vc_zoom/forms.py:37
msgid "Show PIN"
msgstr "Afficher le code confidentiel"
#: indico_vc_zoom/forms.py:39
msgid "Show the VC Room PIN on the event page (insecure!)"
msgstr "Afficher le code confidentiel de la salle Vidyo sur la page de l'événement (peu sûr!)"
#: indico_vc_zoom/forms.py:40
msgid "Show Auto-join URL"
msgstr "Afficher l'URL de connexion"
#: indico_vc_zoom/forms.py:42
msgid "Show the auto-join URL on the event page"
msgstr "Afficher l'URL de connexion sur la page de l'événement"
#: indico_vc_zoom/forms.py:43
msgid "Show Phone Access numbers"
msgstr "Afficher les numéros d'accès téléphonique"
#: indico_vc_zoom/forms.py:45
msgid "Show a link to the list of phone access numbers"
msgstr "Afficher un lien vers la liste des numéros d'accès téléphonique"
#: indico_vc_zoom/forms.py:58 indico_vc_zoom/templates/info_box.html:7
#: indico_vc_zoom/templates/manage_event_info_box.html:6
msgid "Description"
msgstr "Description"
#: indico_vc_zoom/forms.py:58
msgid "The description of the room"
msgstr "La description de la salle"
#: indico_vc_zoom/forms.py:59 indico_vc_zoom/templates/info_box.html:14
#: indico_vc_zoom/templates/manage_event_info_box.html:10
msgid "Owner"
msgstr "Responsable"
#: indico_vc_zoom/forms.py:59
msgid "The owner of the room"
msgstr "Le responsable de la salle"
#: indico_vc_zoom/forms.py:60
#: indico_vc_zoom/templates/manage_event_info_box.html:39
msgid "Moderation PIN"
msgstr "Code confidentiel de modération"
#: indico_vc_zoom/forms.py:61
msgid "Used to moderate the VC Room. Only digits allowed."
msgstr "Utilisé pour modérer la salle de VC. Seuls les chiffres sont autorisés."
#: indico_vc_zoom/forms.py:62 indico_vc_zoom/templates/info_box.html:18
#: indico_vc_zoom/templates/manage_event_info_box.html:32
msgid "Room PIN"
msgstr "Code confidentiel de la salle"
#: indico_vc_zoom/forms.py:63
msgid ""
"Used to protect the access to the VC Room (leave blank for open access). "
"Only digits allowed."
msgstr "Utilisé pour protéger l'accès à la salle de VC (laisser vide pour un accès ouvert). Seuls les chiffres sont autorisés."
#: indico_vc_zoom/forms.py:65
msgid "Auto mute"
msgstr "Coupure automatique des périphériques d'entrée"
#: indico_vc_zoom/forms.py:66
msgid "On"
msgstr "Activé"
#: indico_vc_zoom/forms.py:66
msgid "Off"
msgstr "Désactivé"
#: indico_vc_zoom/forms.py:67
msgid ""
"The VidyoDesktop clients will join the VC room muted by default (audio and "
"video)"
msgstr "Les clients VidyoDesktop rejoindront la salle Vidyo avec le micro et la caméra coupés par défaut"
#: indico_vc_zoom/forms.py:82
msgid "Unable to find this user in Indico."
msgstr "Impossible de trouver cet utilisateur dans Indico."
#: indico_vc_zoom/forms.py:84
msgid "This user does not have a suitable account to use Vidyo."
msgstr "Cet utilisateur n'a pas de compte qui lui permet d'utiliser Vidyo."
#: indico_vc_zoom/plugin.py:49
msgid "Vidyo email support"
msgstr "Adresse électronique de l'assistance Vidyo"
#: indico_vc_zoom/plugin.py:50
msgid "Username"
msgstr "Identifiant"
#: indico_vc_zoom/plugin.py:50
msgid "Indico username for Vidyo"
msgstr "Identifiant Indico pour Vidyo"
#: indico_vc_zoom/plugin.py:51
msgid "Password"
msgstr "Mot de passe"
#: indico_vc_zoom/plugin.py:52
msgid "Indico password for Vidyo"
msgstr "Mot de passe utilisateur pour Vidyo"
#: indico_vc_zoom/plugin.py:53
msgid "Admin API WSDL URL"
msgstr "URL WSDL pour l'API admin"
#: indico_vc_zoom/plugin.py:54
msgid "User API WSDL URL"
msgstr "URL WSDL pour l'API utilisateur"
#: indico_vc_zoom/plugin.py:55
msgid "Indico tenant prefix"
msgstr "Préfixe de tenant pour Indico"
#: indico_vc_zoom/plugin.py:56
msgid "The tenant prefix for Indico rooms created on this server"
msgstr "Le préfixe de tenant pour les salles Vidyo créées sur ce serveur Indico"
#: indico_vc_zoom/plugin.py:57
msgid "Public rooms' group name"
msgstr "Nom du groupe Vidyo pour les salles Vidyo"
#: indico_vc_zoom/plugin.py:58
msgid "Group name for public videoconference rooms created by Indico"
msgstr "Nom du groupe pour les salles publiques de visioconférence créées par Indico"
#: indico_vc_zoom/plugin.py:59
msgid "Authenticators"
msgstr "Services d'authentification"
#: indico_vc_zoom/plugin.py:60
msgid "Identity providers to convert Indico users to Vidyo accounts"
msgstr "Fournisseurs d'identité pour convertir des utilisateurs Indico en comptes Vidyo"
#: indico_vc_zoom/plugin.py:61
msgid "VC room age threshold"
msgstr "Limite d'âge pour les salles Vidyo"
#: indico_vc_zoom/plugin.py:62
msgid ""
"Number of days after an Indico event when a videoconference room is "
"considered old"
msgstr "Nombre de jours à partir de la fin d'un événement dans Indico après lesquels une salle de visioconférence est considérée comme agée"
#: indico_vc_zoom/plugin.py:64
msgid "Max. num. VC rooms before warning"
msgstr "Nombre maximum de salles Vidyo avant un message d'alerte"
#: indico_vc_zoom/plugin.py:65
msgid "Maximum number of rooms until a warning is sent to the managers"
msgstr "Nombre maximum de salles Vidyo créées sur ce serveur avant qu'un message d'alerte soit envoyé aux administrateurs"
#: indico_vc_zoom/plugin.py:66
msgid "VidyoVoice phone number"
msgstr "Numéros de téléphone VidyoVoice"
#: indico_vc_zoom/plugin.py:67
msgid "Link to the list of VidyoVoice phone numbers"
msgstr "Lien vers la liste des numéros de téléphones VidyoVoice"
#: indico_vc_zoom/plugin.py:68
msgid "Client Chooser URL"
msgstr "URL de sélection du client"
#: indico_vc_zoom/plugin.py:69
msgid ""
"URL for client chooser interface. The room key will be passed as a 'url' GET"
" query argument"
msgstr "L'URL pour l'interface de sélection du client. Le code de la salle sera passé comme argument de requête GET."
#: indico_vc_zoom/plugin.py:71
msgid "Creation email footer"
msgstr "Pied de page du courriel de création"
#: indico_vc_zoom/plugin.py:72
msgid "Footer to append to emails sent upon creation of a VC room"
msgstr "Pied de page ajouté au courriel envoyé à la création d'une nouvelle salle Vidyo"
#: indico_vc_zoom/plugin.py:162 indico_vc_zoom/plugin.py:202
#: indico_vc_zoom/plugin.py:240 indico_vc_zoom/plugin.py:269
msgid "No valid Vidyo account found for this user"
msgstr "Pas de compte Vidyo valide pour cet utilisateur"
#: indico_vc_zoom/plugin.py:198 indico_vc_zoom/plugin.py:263
msgid "Room name already in use"
msgstr "Ce nom de salle est déjà utilisé"
#: indico_vc_zoom/plugin.py:213
msgid "Could not find newly created room in Vidyo"
msgstr "Impossible de trouver la nouvelle salle dans Vidyo"
#: indico_vc_zoom/plugin.py:232 indico_vc_zoom/plugin.py:259
#: indico_vc_zoom/plugin.py:288
msgid "This room has been deleted from Vidyo"
msgstr "Cette salle a été supprimée de Vidyo"
#: indico_vc_zoom/templates/buttons.html:7
#, python-format
msgid "You will be the owner of this Vidyo room, replacing %(name)s."
msgstr "Vous deviendrez le responsable de cette salle Vidyo, à la place de %(name)s."
#: indico_vc_zoom/templates/buttons.html:9
msgid "Make me owner"
msgstr "Nommez moi responsable"
#: indico_vc_zoom/templates/buttons.html:19
msgid "Join"
msgstr "Rejoindre"
#: indico_vc_zoom/templates/info_box.html:5
msgid "Name"
msgstr "Nom"
#: indico_vc_zoom/templates/info_box.html:10
#: indico_vc_zoom/templates/manage_event_info_box.html:8
msgid "Extension"
msgstr "Extension"
#: indico_vc_zoom/templates/info_box.html:22
#: indico_vc_zoom/templates/manage_event_info_box.html:45
msgid "Auto-join URL"
msgstr "URL pour connexion à la salle"
#: indico_vc_zoom/templates/info_box.html:29
msgid "Useful links"
msgstr "Liens utiles"
#: indico_vc_zoom/templates/info_box.html:33
msgid "Phone numbers"
msgstr "Numéros de téléphone"
#: indico_vc_zoom/templates/manage_event_info_box.html:18
msgid "Linked to"
msgstr "attachée à"
#: indico_vc_zoom/templates/manage_event_info_box.html:24
msgid "the whole event"
msgstr "l'événement entier"
#: indico_vc_zoom/templates/manage_event_info_box.html:26
msgid "Contribution"
msgstr "La contribution"
#: indico_vc_zoom/templates/manage_event_info_box.html:28
msgid "Session"
msgstr "La session"
#: indico_vc_zoom/templates/manage_event_info_box.html:49
msgid "Created on"
msgstr "Créée le "
#: indico_vc_zoom/templates/manage_event_info_box.html:52
msgid "Modified on"
msgstr "Modifiée le "

90
indico_vc_zoom/util.py Normal file
View File

@ -0,0 +1,90 @@
from __future__ import unicode_literals
import re
from flask_multipass import IdentityRetrievalFailed
from indico.core.auth import multipass
from indico.core.db import db
from indico.modules.auth import Identity
from indico.modules.users import User
authenticators_re = re.compile(r'\s*,\s*')
def iter_user_identities(user):
"""Iterates over all existing user identities that can be used with Zoom"""
from indico_vc_zoom.plugin import ZoomPlugin
providers = authenticators_re.split(ZoomPlugin.settings.get('authenticators'))
done = set()
for provider in providers:
for _, identifier in user.iter_identifiers(check_providers=True, providers={provider}):
if identifier in done:
continue
done.add(identifier)
yield identifier
def get_user_from_identifier(settings, identifier):
"""Get an actual User object from an identifier"""
providers = list(auth.strip() for auth in settings.get('authenticators').split(','))
identities = Identity.find_all(Identity.provider.in_(providers), Identity.identifier == identifier)
if identities:
return sorted(identities, key=lambda x: providers.index(x.provider))[0].user
for provider in providers:
try:
identity_info = multipass.get_identity(provider, identifier)
except IdentityRetrievalFailed:
continue
if identity_info is None:
continue
if not identity_info.provider.settings.get('trusted_email'):
continue
emails = {email.lower() for email in identity_info.data.getlist('email') if email}
if not emails:
continue
user = User.find_first(~User.is_deleted, User.all_emails.in_(list(emails)))
if user:
return user
def iter_extensions(prefix, event_id):
"""Return extension (prefix + event_id) with an optional suffix which is
incremented step by step in case of collision
"""
extension = '{prefix}{event_id}'.format(prefix=prefix, event_id=event_id)
yield extension
suffix = 1
while True:
yield '{extension}{suffix}'.format(extension=extension, suffix=suffix)
suffix += 1
def update_room_from_obj(settings, vc_room, room_obj):
"""Updates a VCRoom DB object using a SOAP room object returned by the API"""
vc_room.name = room_obj.name
if room_obj.ownerName != vc_room.data['owner_identity']:
owner = get_user_from_identifier(settings, room_obj.ownerName) or User.get_system_user()
vc_room.zoom_meeting.owned_by_user = owner
vc_room.data.update({
'description': room_obj.description,
'zoom_id': unicode(room_obj.roomID),
'url': room_obj.RoomMode.roomURL,
'owner_identity': room_obj.ownerName,
'room_pin': room_obj.RoomMode.roomPIN if room_obj.RoomMode.hasPIN else "",
'moderation_pin': room_obj.RoomMode.moderatorPIN if room_obj.RoomMode.hasModeratorPIN else "",
})
vc_room.zoom_meeting.extension = int(room_obj.extension)
def retrieve_principal(principal):
from indico.modules.users import User
type_, id_ = principal
if type_ in {'Avatar', 'User'}:
return User.get(int(id_))
else:
raise ValueError('Unexpected type: {}'.format(type_))

29
setup.py Normal file
View File

@ -0,0 +1,29 @@
from __future__ import unicode_literals
from setuptools import find_packages, setup
setup(
name='indico-plugin-vc-zoom',
version='0.2-dev',
description='Zoom video-conferencing plugin for Indico',
url='',
license='MIT',
author='Giovanni Mariano - ENEA',
author_email='giovanni.mariano@enea.it',
packages=find_packages(),
zip_safe=False,
include_package_data=True,
install_requires=[
'indico>=2',
'requests',
'PyJWT'
],
classifiers=[
'Environment :: Plugins',
'Environment :: Web Environment',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2.7'
],
entry_points={'indico.plugins': {'vc_zoom = indico_vc_zoom.plugin:ZoomPlugin'}}
)

24
tests/task_test.py Normal file
View File

@ -0,0 +1,24 @@
from datetime import datetime
import pytest
from pytz import utc
from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomEventAssociation, VCRoomStatus
from indico_vc_zoom.models.zoom_meetings import ZoomMeeting
@pytest.fixture
def create_dummy_room(db, dummy_user):
"""Returns a callable which lets you create dummy Zoom room occurrences"""
pass
def test_room_cleanup(create_event, create_dummy_room, freeze_time, db):
"""Test that 'old' Zoom rooms are correctly detected"""
freeze_time(datetime(2015, 2, 1))
pass
assert {r.id for r in find_old_zoom_rooms(180)} == {2, 3, 5}

30163
url_map.json Normal file

File diff suppressed because it is too large Load Diff

5
webpack-bundles.json Normal file
View File

@ -0,0 +1,5 @@
{
"entry": {
"main": "./index.js"
}
}