Zoom web hooks endpoint - refs BT#17288

Recordings are listed in the database as soon as they are available on
the remote Zoom servers.

Meeting and recording lists are kept up-to-date.

This also allows meeting participant attendance tracking (not
implemented yet).
pull/3383/head
Sébastien Ducoulombier 5 years ago
parent 9ca1c03ff3
commit 83db53798f
  1. 5
      plugin/zoom/Entity/MeetingEntity.php
  2. 4
      plugin/zoom/admin.php
  3. 100
      plugin/zoom/endpoint.php
  4. 21
      plugin/zoom/lang/english.php
  5. 20
      plugin/zoom/lang/french.php
  6. 21
      plugin/zoom/lang/spanish.php
  7. 16
      plugin/zoom/lib/API/JsonDeserializableTrait.php
  8. 3
      plugin/zoom/lib/API/MeetingSettings.php
  9. 13
      plugin/zoom/lib/zoom_plugin.class.php
  10. 4
      plugin/zoom/user.php

@ -341,6 +341,11 @@ class MeetingEntity
return is_null($this->user) && is_null($this->course);
}
public function setStatus($status)
{
$this->meetingInfoGet->status = $status;
}
/**
* Builds the list of users that can register into this meeting.
* Zoom requires an email address, therefore users without an email address are excluded from the list.

@ -18,10 +18,6 @@ $this_section = SECTION_PLATFORM_ADMIN;
$form = $plugin->getAdminSearchForm();
$startDate = new DateTime($form->getSubmitValue('start'));
$endDate = new DateTime($form->getSubmitValue('end'));
$reloadRecordingLists = $form->getSubmitValue('reloadRecordingLists');
if ($reloadRecordingLists) {
$plugin->reloadPeriodRecordings($startDate, $endDate);
}
$tpl = new Template($tool_name);
$tpl->assign('meetings', $plugin->getMeetingRepository()->periodMeetings($startDate, $endDate));

@ -0,0 +1,100 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Zoom\API\MeetingInfoGet;
use Chamilo\PluginBundle\Zoom\API\RecordingMeeting;
use Chamilo\PluginBundle\Zoom\MeetingEntity;
use Chamilo\PluginBundle\Zoom\RecordingEntity;
use Chamilo\PluginBundle\Zoom\RegistrantEntity;
if ('POST' !== $_SERVER['REQUEST_METHOD']) {
http_response_code(404); // Not found
exit;
}
$authorizationHeaderValue = apache_request_headers()['authorization']; // TODO handle non-apache installations
require __DIR__.'/config.php';
if (api_get_plugin_setting('zoom', 'verificationToken') !== $authorizationHeaderValue) {
http_response_code(401); // Unauthorized
exit;
}
$body = file_get_contents('php://input');
$decoded = json_decode($body);
if (is_null($decoded) || !is_object($decoded) || !isset($decoded->event) || !isset($decoded->payload->object)) {
error_log(sprintf('Did not recognize event notification: %s', $body));
http_response_code(422); // Unprocessable Entity
exit;
}
$object = $decoded->payload->object;
list($objectType, $action) = explode('.', $decoded->event);
switch ($objectType) {
case 'meeting':
$meetingRepository = $entityManager->getRepository(MeetingEntity::class);
$registrantRepository = $entityManager->getRepository(RegistrantEntity::class);
switch ($action) {
case 'deleted':
$meetingEntity = $meetingRepository->find($object->id);
if (!is_null($meetingEntity)) {
$entityManager->remove($meetingEntity);
}
break;
case 'ended':
case 'started':
$entityManager->persist(
(
$meetingRepository->find($object->id)->setStatus($action)
?: (new MeetingEntity)->setMeetingInfoGet(MeetingInfoGet::fromObject($object))
)
);
$entityManager->flush();
break;
case 'participant_joined':
case 'participant_left':
$registrant = $registrantRepository->find($object->participant->id);
if (!is_null($registrant)) {
// TODO log attendance
}
break;
case 'alert':
default:
error_log(sprintf('Event "%s" on %s was unhandled: %s', $action, $objectType, $body));
http_response_code(501); // Not Implemented
}
break;
case 'recording':
$recordingRepository = $entityManager->getRepository(RecordingEntity::class);
switch ($action) {
case 'completed':
$entityManager->persist(
(new RecordingEntity())->setRecordingMeeting(RecordingMeeting::fromObject($object))
);
$entityManager->flush();
break;
case 'recovered':
if (is_null($recordingRepository->find($object->uuid))) {
$entityManager->persist(
(new RecordingEntity())->setRecordingMeeting(RecordingMeeting::fromObject($object))
);
$entityManager->flush();
}
break;
case 'trashed':
case 'deleted':
$recordingEntity = $recordingRepository->find($object->uuid);
if (!is_null($recordingEntity)) {
$entityManager->remove($recordingEntity);
$entityManager->flush();
}
break;
default:
error_log(sprintf('Event "%s" on %s was unhandled: %s', $action, $objectType, $body));
http_response_code(501); // Not Implemented
}
break;
default:
error_log(sprintf('Event "%s" on %s was unhandled: %s', $action, $objectType, $body));
http_response_code(501); // Not Implemented
}

@ -8,6 +8,7 @@ $strings['plugin_comment'] = "Zoom Videoconference integration in courses and se
$strings['tool_enable'] = 'Zoom videoconference tool enabled';
$strings['apiKey'] = 'API Key';
$strings['apiSecret'] = 'API Secret';
$strings['verificationToken'] = 'Verification Token';
$strings['enableParticipantRegistration'] = 'Enable participant registration';
$strings['enableCloudRecording'] = 'Enable cloud recording';
$strings['enableGlobalConference'] = 'Enable global conference';
@ -28,10 +29,24 @@ To get them, create a <em>JWT App</em> :
<br/>2. click on <em>Advanced / Application Marketplace</em>
<br/>3. click on <em><a href=\"https://marketplace.zoom.us/develop/create\">Develop / build App</a></em>
<br/>4. choose <em>JWT / Create</em>
<br/>5. fill in information about your \"App\"
<br/>5. Information: fill in fields about your \"App\"
(application and company names, contact name and email address)
<br/>6. click on <em>Continue</em>
Locate your API Key and Secret in the App Credentials page.
<br/>6. Click on <em>Continue</em>
<br/>7. App Credentials: <strong>copy your API Key and Secret to these fields below</strong>
<br/>8. click on <em>Continue</em>
<br/>9. Feature:
enable <em>Event Subscriptions</em> to add a new one with endpoint URL
<code>https://your.chamilo.url/plugin/zoom/endpoint.php</code>
and add these event types:
<br/>- Start Meeting
<br/>- End Meeting
<br/>- Participant/Host joined meeting
<br/>- Participant/Host left meeting
<br/>- All Recordings have completed
<br/>- Recording transcript files have completed
<br/>then click on <em>Done</em> then on <em>Save</em>
and <strong>copy your Verification Token to the field below</strong>.
<br/>10. click on <em>Continue</em>
<br/>
<strong>Attention</strong>:
<br/>Zoom is <em>NOT</em> free software and specific rules apply to personal data protection.

@ -26,10 +26,25 @@ Pour les obtenir, créez une <em>JWT app</em> :
<br/>2. cliquez sur <em>Avancé / Marketplace d'application</em>
<br/>3. cliquez sur <em><a href=\"https://marketplace.zoom.us/develop/create\">Develop / build App</a></em>
<br/>4. choisissez <em>JWT / Create</em>
<br/>5. saisissez quelques informations sur votre \"App\"
<br/>5. Information: remplissez quelques champs à propos de votre \"App\"
(noms de l'application, de l'entreprise, nom et adresse de courriel de contact)
<br/>6. cliquez sur <em>Continue</em>
<br/>La page <em>App Credentials</em> affiche la clé (API Key) et le code secret (API Secret) à saisir ici.
<br/>7. App Credentials :
<strong>copiez la clé (API Key) et le code secret (API Secret) dans les champs ci-dessous.</strong>
<br/>8. cliquez sur <em>Continue</em>
<br/>9. Feature :
activez <em>Event Subscriptions</em> pour en ajouter une avec comme endpoint URL
<code>https://your.chamilo.url/plugin/zoom/endpoint.php</code>
et ajoutez ces types d'événements :
<br/>- Start Meeting
<br/>- End Meeting
<br/>- Participant/Host joined meeting
<br/>- Participant/Host left meeting
<br/>- All Recordings have completed
<br/>- Recording transcript files have completed
<br/>puis cliquez sur <em>Done</em> puis sur <em>Save</em>
et <strong>copiez votre Verification Token dans le champ ci-dessous</strong>.
<br/>10. cliquez sur <em>Continue</em>
<br/>
<strong>Attention</strong> :
<br/>Zoom n'est <em>PAS</em> un logiciel libre
@ -108,6 +123,7 @@ $strings['UpdateMeeting'] = "Mettre à jour la conférence";
$strings['UpdateRegisteredUserList'] = "Mettre à jour la liste des utilisateurs inscrits";
$strings['UserRegistration'] = "Inscription des utilisateurs";
$strings['Y-m-d H:i'] = "d/m/Y à H\hi";
$strings['verificationToken'] = 'Verification Token';
$strings['Waiting'] = "en attente";
$strings['XRecordingOfMeetingXFromXDurationXDotX'] = "Enregistrement (%s) de la conférence %s de %s (%s).%s";
$strings['YouAreNotRegisteredToThisMeeting'] = "Vous n'êtes pas inscrit à cette conférence";

@ -26,10 +26,26 @@ Para obtenerlos, crea una <em>app JWT</em> :
<br/>2. de clic en <em>Avanzado / Marketplace de aplicaciones</em>
<br/>3. de clic en <em><a href=\"https://marketplace.zoom.us/develop/create\">Develop / build App</a></em>
<br/>4. escoja <em>JWT / Create</em>
<br/>5. ingrese algunas informaciones sobre vuestra \"App\"
<br/>5. Information: ingrese algunas informaciones sobre vuestra \"App\"
(nombres de la aplicación, de la empresa, nombre y dirección de correo de contacto)
<br/>6. de clic en <em>Continue</em>
<br/>La página <em>App Credentials</em> muestra la clave (API Key) y el código secreto (API Secret) por ingresar aquí.
<br/>7. App Credentials:
muestra la clave (API Key) y el código secreto (API Secret) por ingresar aquí.
<strong>copiez la clé (API Key) et le code secret (API Secret) dans les champs ci-dessous.</strong>
<br/>8. de clic en <em>Continue</em>
<br/>9. Feature :
activez <em>Event Subscriptions</em> para agregar uno con su endpoint URL
<code>https://your.chamilo.url/plugin/zoom/endpoint.php</code>
y agrega este tipo de eventos:
<br/>- Start Meeting
<br/>- End Meeting
<br/>- Participant/Host joined meeting
<br/>- Participant/Host left meeting
<br/>- All Recordings have completed
<br/>- Recording transcript files have completed
<br/>de clic en <em>Done</em> y luego en <em>Save</em>
y <strong>copie su Verification Token en el campo a continuación</strong>.
<br/>10. de clic en <em>Continue</em>
<br/>
<strong>Atención</strong> :
<br/>Zoom <em>NO ES</em> un software libre, y reglas específicas de protección de datos se aplican a este.
@ -107,6 +123,7 @@ $strings['UpdateMeeting'] = "Actualizar la conferencia";
$strings['UpdateRegisteredUserList'] = "Actualizar la lista de usuarios inscritos";
$strings['UserRegistration'] = "Inscripción de los usuarios";
$strings['Y-m-d H:i'] = "d/m/Y a las H\hi";
$strings['verificationToken'] = 'Verification Token';
$strings['Waiting'] = "en espera";
$strings['XRecordingOfMeetingXFromXDurationXDotX'] = "Grabación (%s) de la conferencia %s de %s (%s).%s";
$strings['YouAreNotRegisteredToThisMeeting'] = "No estás registrado en esta reunión";

@ -32,6 +32,20 @@ trait JsonDeserializableTrait
throw new Exception('Could not decode JSON: '.$json);
}
return static::fromObject($object);
}
/**
* Builds a class instance from an already json-decoded object.
*
* @param object $object
*
* @throws Exception on unexpected object property
*
* @return static
*/
public static function fromObject($object)
{
$instance = new static();
static::recursivelyCopyObjectProperties($object, $instance);
@ -101,7 +115,7 @@ trait JsonDeserializableTrait
$destination->$name = $value;
}
} else {
throw new Exception("Source object has property $name, which was not expected.");
error_log("Source object has property $name, which was not expected: ".json_encode($source));
}
}
$destination->initializeExtraProperties();

@ -80,6 +80,9 @@ class MeetingSettings
/** @var bool Enable waiting room */
public $waiting_room;
/** @var bool undocumented */
public $request_permission_to_unmute_participants;
/** @var string[] List of global dial-in countries */
public $global_dial_in_countries;

@ -47,6 +47,7 @@ class ZoomPlugin extends Plugin
'tool_enable' => 'boolean',
'apiKey' => 'text',
'apiSecret' => 'text',
'verificationToken' => 'text',
'enableParticipantRegistration' => 'boolean',
'enableCloudRecording' => 'boolean',
'enableGlobalConference' => 'boolean',
@ -221,9 +222,6 @@ class ZoomPlugin extends Plugin
$form = new FormValidator('search');
$form->addDatePicker('start', get_lang('StartDate'));
$form->addDatePicker('end', get_lang('EndDate'));
// TODO instead of requiring user intervention, implement Zoom API callbacks:
// TODO https://marketplace.zoom.us/docs/guides/build/webhook-only-app
$form->addCheckBox('reloadRecordingLists', '', get_lang('ReloadRecordingLists'));
$form->addButtonSearch(get_lang('Search'));
$oneMonth = new DateInterval('P1M');
if ($form->validate()) {
@ -249,7 +247,6 @@ class ZoomPlugin extends Plugin
$form->setDefaults([
'start' => $start->format('Y-m-d'),
'end' => $end->format('Y-m-d'),
'reloadRecordingLists' => false,
]);
} catch (Exception $exception) {
error_log(join(':', [__FILE__, __LINE__, $exception]));
@ -851,10 +848,16 @@ class ZoomPlugin extends Plugin
}
/**
* Update local recording list from remote Zoom server's version.
* Kept to implement a future administration button ("import existing data from zoom server")
*
* @param DateTime $startDate
* @param DateTime $endDate
*
* @throws OptimisticLockException
* @throws Exception
*/
public function reloadPeriodRecordings(DateTime $startDate, DateTime $endDate)
public function reloadPeriodRecordings($startDate, $endDate)
{
foreach (RecordingList::loadPeriodRecordings($startDate, $endDate) as $recordingMeeting) {
$recordingEntity = $this->getRecordingRepository()->find($recordingMeeting->uuid);

@ -17,10 +17,6 @@ $user = api_get_user_entity(api_get_user_id());
$form = $plugin->getAdminSearchForm();
$startDate = new DateTime($form->getElement('start')->getValue());
$endDate = new DateTime($form->getElement('end')->getValue());
$reloadRecordingLists = $form->getElement('reloadRecordingLists')->getValue();
if ($reloadRecordingLists) {
$plugin->reloadPeriodRecordings($startDate, $endDate);
}
$tpl = new Template();
$tpl->assign('meetings', $plugin->getMeetingRepository()->periodUserMeetings($startDate, $endDate, $user));

Loading…
Cancel
Save