You are here

zoomapi.module in Zoom API 7.2

Same filename and directory in other branches
  1. 7 zoomapi.module

Main file for the Zoom API module.

File

zoomapi.module
View source
<?php

/**
 * @file
 * Main file for the Zoom API module.
 */
define('ZOOMAPI_USER_TYPE_BASIC', 1);
define('ZOOMAPI_USER_TYPE_PRO', 2);
define('ZOOMAPI_USER_TYPE_CORP', 3);
define('ZOOMAPI_USER_TYPE_DEFAULT', ZOOMAPI_USER_TYPE_BASIC);
define('ZOOMAPI_USER_EMAIL_PATTERN_DEFAULT', 'zoomuser_[user:uid]@[site:url-brief]');
define('ZOOMAPI_MEETING_TYPE_INSTANT', 1);
define('ZOOMAPI_MEETING_TYPE_NORMAL', 2);
define('ZOOMAPI_MEETING_TYPE_RECURRING_NO_FIXED_TIME', 3);
define('ZOOMAPI_MEETING_TYPE_RECURRING_FIXED_TIME', 8);
define('ZOOMAPI_MEETING_TYPE_DEFAULT', ZOOMAPI_MEETING_TYPE_NORMAL);
define('ZOOMAPI_MEETING_TIME_FORMAT_LOCAL', 'Y-m-d\\TH:i:s');
define('ZOOMAPI_MEETING_TIME_FORMAT_DEFAULT', 'Y-m-d\\TH:i:s\\Z');
define('ZOOMAPI_MEETING_TIME_FORMAT_TZ', 'GMT');

/**
 * Extend SystemQueue making each item unique.
 */
class ZoomAPISystemQueue extends SystemQueue {

  /**
   * Override createItem to handle merging instead of inserting.
   */
  public function createItem($data) {
    $serial_data = serialize($data);
    $query = db_merge('queue')
      ->key([
      'name' => $this->name,
      'data' => $serial_data,
    ])
      ->fields([
      'name' => $this->name,
      'data' => $serial_data,
      'created' => REQUEST_TIME,
    ]);
    return (bool) $query
      ->execute();
  }

}

/**
 * Implements hook_permission().
 */
function zoomapi_permission() {
  return [
    'administer zoomapi' => [
      'title' => t('Administer Zoom API'),
      'description' => t('Administer the Zoom API settings.'),
      'restrict access' => TRUE,
    ],
    'access zoomapi reports' => [
      'title' => t('Access Zoom API Reports'),
      'description' => t('Access the Zoom API reports.'),
      'restrict access' => TRUE,
    ],
  ];
}

/**
 * Implements hook_menu().
 */
function zoomapi_menu() {

  // Webhook.
  // @todo allow for custom webhook urls.
  $items['zoomapi/webhook'] = [
    'title' => 'Zoom API Webhook',
    'description' => 'See https://zoom.github.io/api/#webhooks',
    'page callback' => 'zoomapi_webhooks_callback',
    'access callback' => 'zoomapi_webhooks_access',
    'file' => 'zoomapi.webhooks.inc',
    'type' => MENU_CALLBACK,
  ];

  // Admin settings.
  $items['admin/config/services/zoomapi'] = [
    'title' => 'Zoom API',
    'description' => 'Configuration for Zoom API',
    'page callback' => 'drupal_get_form',
    'page arguments' => [
      'zoomapi_settings_form',
    ],
    'access arguments' => [
      'administer zoomapi',
    ],
    'file' => 'zoomapi.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  ];
  $items['admin/config/services/zoomapi/settings'] = [
    'title' => 'Settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  ];

  // Admin Webhooks.
  $items['admin/config/services/zoomapi/webhooks'] = [
    'title' => 'Webhooks',
    'description' => 'Manage existing and create new webhooks.',
    'page callback' => 'drupal_get_form',
    'page arguments' => [
      'zoomapi_report_custom_list_form',
      'webhooks',
    ],
    'access arguments' => [
      'administer zoomapi',
    ],
    'file' => 'zoomapi.admin.inc',
    'type' => MENU_LOCAL_TASK,
  ];

  // Report Listing.
  $items['admin/reports/zoomapi'] = [
    'title' => 'Zoom API',
    'description' => 'A few basic custom reports and API reports.',
    'position' => 'left',
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => [
      'access zoomapi reports',
    ],
    'file' => 'system.admin.inc',
    'file path' => drupal_get_path('module', 'system'),
    'type' => MENU_NORMAL_ITEM,
  ];

  // Report: Custom Lists.
  // @todo unable to do this with meetings at the moment and webhooks is
  // displayed in configuration.
  foreach ([
    'users',
  ] as $api) {
    $label = ucfirst($api);
    $items["admin/reports/zoomapi/{$api}"] = [
      'title' => $label,
      'description' => "Basic listing of Zoom {$label}.",
      'page callback' => 'drupal_get_form',
      'page arguments' => [
        'zoomapi_report_custom_list_form',
        $api,
      ],
      'access arguments' => [
        'access zoomapi reports',
      ],
      'file' => 'zoomapi.admin.inc',
      'type' => MENU_NORMAL_ITEM,
    ];
  }
  return $items;
}

/**
 * Access callback: Zoom API Webhooks.
 */
function zoomapi_webhooks_access() {

  // Only allow POST requests.
  $http_verb = strtoupper($_SERVER['REQUEST_METHOD']);
  if ($http_verb != 'POST') {
    return FALSE;
  }

  // Webhooks not enabled.
  if (!variable_get('zoomapi_webhooks_enabled', FALSE)) {
    return FALSE;
  }

  // Validate basic auth.
  // @todo validate against different username/pw for each webhook.
  if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW']) && $_SERVER['PHP_AUTH_USER'] == variable_get('zoomapi_webhooks_username', FALSE) && $_SERVER['PHP_AUTH_PW'] == variable_get('zoomapi_webhooks_password', FALSE)) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implements hook_user_insert().
 */
function zoomapi_user_insert(&$edit, $account, $category) {
  if (!variable_get('zoomapi_create_on_new_user', FALSE)) {
    return;
  }
  if (variable_get('zoomapi_create_on_new_user', FALSE) && !zoomapi_user_email_exists($account->mail)) {
    zoomapi_create_user($account);
  }
}

/**
 * Implements hook_cron_queue_info().
 */
function zoomapi_cron_queue_info() {
  $info['zoomapi_download_meeting_recordings_to_entity_queue'] = [
    'worker callback' => 'zoomapi_download_meeting_recordings_to_entity_worker',
    'time' => 60,
  ];
  return $info;
}

/**
 * Implements hook_cron().
 */
function zoomapi_cron() {
  $process_downloads = variable_get('zoomapi_recordings_download_process', 'cron');
  if ($process_downloads == 'cron') {
    $meeting_recordings_trackers = zoomapi_get_unsuccessful_recording_download_tracking_info();
    if (!$meeting_recordings_trackers) {
      return;
    }
    foreach ($meeting_recordings_trackers as $tracker) {
      zoomapi_download_meeting_recordings_from_tracker($tracker);
    }
  }
  elseif ($process_downloads == 'queue') {
    if ($uuids = zoomapi_get_unsuccessful_recording_download_uuids()) {
      $queue = DrupalQueue::get('zoomapi_download_meeting_recordings_to_entity_queue');
      foreach ($uuids as $uuid) {
        $queue
          ->createItem($uuid);
      }
    }
  }

  // Remove old webhook events.
  $retention_days = variable_get('zoomapi_retain_webhook_days', 30);
  $expire_ts = REQUEST_TIME - $retention_days * 24 * 60 * 60;
  db_delete('zoomapi_webhooks_log')
    ->condition('created', $expire_ts, '<')
    ->execute();

  // Remove old download trackers.
  $retention_days = variable_get('zoomapi_recordings_download_retention_days', 60);
  $expire_ts = REQUEST_TIME - $retention_days * 24 * 60 * 60;
  db_delete('zoomapi_recordings_download_tracker')
    ->condition('created', $expire_ts, '<')
    ->execute();

  // Remove old meeting index info.
  $retention_days = variable_get('zoomapi_retain_meeting_index_days', 30);
  $expire_ts = REQUEST_TIME - $retention_days * 24 * 60 * 60;
  $expire_dt = new DateTime('@' . $expire_ts);
  $expire_iso = $expire_dt
    ->format('Y-m-d\\TH:i:s\\Z');
  db_delete('zoomapi_meetings_index')
    ->condition('start_time', '', '!=')
    ->condition('start_time', $expire_iso, '<')
    ->execute();
  db_delete('zoomapi_meetings_index')
    ->condition('start_time', '')
    ->condition('created', $expire_ts, '<')
    ->execute();
}

/**
 * Implements hook_zoomapi_user_create().
 *
 * The user update api allows for more user properties than the user create api
 * so we hook into the user create to call the user update immediately
 * afterwards.
 */
function zoomapi_zoomapi_user_create($zoom_user, $account) {
  $user_info['timezone'] = zoomapi_get_account_timezone($account);
  zoomapi_update_user($account, $user_info);
}

/**
 * Get ZoomAPI client.
 */
function zoomapi_client() {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  return zoomapi_api_client();
}

/**
 * Check if email in use.
 *
 * Checks against local table, however option to check against API.
 */
function zoomapi_user_email_exists($email, $api_check = FALSE) {
  try {
    $exists = FALSE;
    if (!$api_check) {
      $user_info = zoomapi_get_zoom_user_tracker_info($email);
      $exists = !empty($user_info['email']);
    }
    else {
      $exists = zoomapi_api_user_email_exists($email);
    }
    return $exists;
  } catch (\Exception $e) {
    return FALSE;
  }
}

/**
 * Create Zoom user.
 *
 * Simple wrapper to the API so we can track drupal/zoom user account
 * relationship.
 */
function zoomapi_create_user($account, array $user_info = [], $create_action = '') {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  $uid = is_numeric($account) ? $account : $account->uid;
  if (empty($user_info['type'])) {
    $user_info['type'] = variable_get('zoomapi_user_create_type_default', ZOOMAPI_USER_TYPE_DEFAULT);
  }
  if (empty($user_info['email'])) {
    $account = is_numeric($account) ? user_load($account) : $account;
    $user_info['email'] = variable_get('zoomapi_use_account_email', FALSE) ? $account->mail : zoomapi_generate_user_email($account);
  }
  $params['action'] = $create_action ?: variable_get('zoomapi_user_create_action_default', 'custCreate');
  $params['user_info'] = $user_info;
  if (zoomapi_user_email_exists($params['user_info']['email'])) {
    watchdog(__FUNCTION__, 'Unable to create a Zoom user. The email @email is already in use.', [
      '@email' => $params['user_info']['email'],
    ], WATCHDOG_ERROR);
    return FALSE;
  }
  return zoomapi_api_create_user($uid, $params);
}

/**
 * Generate zoom user email.
 */
function zoomapi_generate_user_email($account) {
  $account = is_numeric($account) ? user_load($account) : $account;
  $mail_pattern = variable_get('zoomapi_user_email_pattern', ZOOMAPI_USER_EMAIL_PATTERN_DEFAULT);
  $token_data = [
    'user' => $account,
  ];
  $email = token_replace($mail_pattern, $token_data);
  return $email;
}

/**
 * Get Zoom users.
 */
function zoomapi_get_users() {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  $zoom_users = zoomapi_api_get_users();
  return $zoom_users;
}

/**
 * Get Zoom user.
 */
function zoomapi_get_user($account, $autocreate = NULL) {
  try {
    module_load_include('inc', 'zoomapi', 'zoomapi.api');
    $autocreate = !is_null($autocreate) ? $autocreate : variable_get('zoomapi_autocreate_on_get', FALSE);
    $uid = is_numeric($account) ? $account : $account->uid;
    $zoom_user_id = zoomapi_get_zoom_user_id($uid);
    $zoom_email = '';
    $zoom_user = [];
    if ($zoom_user_id) {
      $zoom_user = zoomapi_api_get_user($zoom_user_id);
    }
    if (!$zoom_user && ($zoom_email = zoomapi_generate_user_email($account))) {
      $zoom_user = zoomapi_api_get_user($zoom_email, $uid);
    }
    if (!$zoom_user && $autocreate) {
      $zoom_user = zoomapi_create_user($account);
    }
    return $zoom_user;
  } catch (\Exception $e) {
    watchdog(__FUNCTION__, 'Unable to retrieve zoom account @zoom_acct for user !uid. Error: @e', [
      '@e' => $e
        ->getMessage(),
      '@zoom_acct' => $zoom_user_id,
      '!uid' => $uid,
    ], WATCHDOG_ERROR);
    return [];
  }
}

/**
 * Update Zoom user.
 */
function zoomapi_update_user($account, array $user_info) {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  $uid = is_numeric($account) ? $account : $account->uid;
  $zoom_user_id = zoomapi_get_zoom_user_id($uid);
  if (!$zoom_user_id) {
    watchdog(__FUNCTION__, 'Unable to update Zoom account for user !uid. Unable to lookup Zoom account ID.', [
      '!uid' => $uid,
    ], WATCHDOG_ERROR);
    return FALSE;
  }
  return zoomapi_api_update_user($uid, $zoom_user_id, $user_info);
}

/**
 * Update Zoom user email.
 *
 * @todo "Domain name doesn't match, please contact Zoom customer support to set
 * managed domains for your account"
 */
function zoomapi_update_user_email($account, $email) {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  $uid = is_numeric($account) ? $account : $account->uid;
  $zoom_user_id = zoomapi_get_zoom_user_id($uid);
  if (!$zoom_user_id) {
    watchdog(__FUNCTION__, 'Unable to change Zoom account email to @email for user !uid. Unable to lookup Zoom account ID.', [
      '@email' => $email,
      '!uid' => $uid,
    ], WATCHDOG_ERROR);
    return FALSE;
  }
  return zoomapi_api_update_user_email($zoom_user_id, $email);
}

/**
 * Track Drupal / Zoom user account relationship.
 */
function zoomapi_track_user($uid, array $zoom_user) {
  $realm = zoomapi_realm();
  $record = [
    'uid' => $uid,
    'realm' => $realm,
    'zoom_user_id' => $zoom_user['id'],
    'zoom_email' => $zoom_user['email'],
  ];
  $exists = db_query("\n    SELECT\n      uid,\n      realm,\n      zoom_user_id,\n      zoom_email\n    FROM {zoomapi_users}\n    WHERE zoom_email = :email\n    AND realm = :realm\n  ", [
    ':email' => $record['zoom_email'],
    ':realm' => $realm,
  ])
    ->fetchAssoc();
  if ($exists) {
    if ($record !== $exists) {
      $record['changed'] = REQUEST_TIME;
      drupal_write_record('zoomapi_users', $record, [
        'email',
      ]);
    }
  }
  else {
    $record['created'] = REQUEST_TIME;
    $record['changed'] = REQUEST_TIME;
    drupal_write_record('zoomapi_users', $record);
  }
}

/**
 * Get Zoom Users Tracker Info.
 */
function zoomapi_get_zoom_users_tracker_info(array $uids_or_emails = []) {
  $realm = zoomapi_realm();
  $sql = "\n    SELECT\n      uid,\n      realm,\n      zoom_user_id,\n      zoom_email\n    FROM {zoomapi_users}\n    WHERE realm = :realm\n  ";
  $sql_args = [
    ':realm' => $realm,
  ];
  if ($uids_or_emails) {
    $sql .= "\n      AND (\n        uid IN (:uids)\n        OR zoom_email IN (:emails)\n      )\n    ";
    $sql_args += [
      ':uids' => $uids_or_emails,
      ':emails' => $uids_or_emails,
    ];
  }
  $results = db_query($sql, $sql_args)
    ->fetchAllAssoc('zoom_email');
  return $results;
}

/**
 * Get Drupal uid from Zoom user ID.
 */
function zoomapi_get_user_from_zoom_userid($zoom_user_id) {
  $uid = db_query("\n    SELECT\n      uid\n    FROM {zoomapi_users}\n    WHERE zoom_user_id = :zoom_user_id\n    AND realm = :realm\n  ", [
    ':zoom_user_id' => $zoom_user_id,
    ':realm' => zoomapi_realm(),
  ])
    ->fetchField();
  return $uid ?: 0;
}

/**
 * Get Zoom User Tracker Info.
 */
function zoomapi_get_zoom_user_tracker_info($uid_or_email) {
  $info = zoomapi_get_zoom_users_tracker_info([
    $uid_or_email,
  ]);
  return $info ? (array) reset($info) : FALSE;
}

/**
 * Get Zoom User ID.
 */
function zoomapi_get_zoom_user_id($uid) {
  $info = zoomapi_get_zoom_user_tracker_info($uid);
  return !empty($info['zoom_user_id']) ? $info['zoom_user_id'] : FALSE;
}

/**
 * Get Zoom User Email.
 */
function zoomapi_get_zoom_user_email($uid) {
  $info = zoomapi_get_zoom_user_tracker_info($uid);
  return !empty($info['zoom_email']) ? $info['zoom_email'] : FALSE;
}

/**
 * Get Zoom User settings.
 */
function zoomapi_get_user_settings($account) {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  $uid = is_numeric($account) ? $account : $account->uid;
  $zoom_user_id = zoomapi_get_zoom_user_id($uid);
  if (!$zoom_user_id) {
    watchdog(__FUNCTION__, 'Unable to retrieve Zoom user settings for user !uid. Unable to lookup Zoom account ID.', [
      '!uid' => $uid,
    ], WATCHDOG_ERROR);
    return [];
  }
  return zoomapi_api_get_user_settings($zoom_user_id);
}

/**
 * Update Zoom User settings.
 */
function zoomapi_update_user_settings($account, array $settings) {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  $uid = is_numeric($account) ? $account : $account->uid;
  $zoom_user_id = zoomapi_get_zoom_user_id($uid);
  if (!$zoom_user_id) {
    watchdog(__FUNCTION__, 'Unable to update Zoom user settings for user !uid. Unable to lookup Zoom account ID.', [
      '!uid' => $uid,
    ], WATCHDOG_ERROR);
    return [];
  }
  return zoomapi_api_update_user_settings($zoom_user_id, $settings);
}

/**
 * Create Zoom Meeting.
 */
function zoomapi_create_meeting($uid, array $params, array $context = []) {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  $zoom_user_id = zoomapi_get_zoom_user_id($uid);
  if (!$zoom_user_id) {
    watchdog(__FUNCTION__, 'Unable to create Zoom meeting for user !uid. Unable to lookup Zoom account ID.', [
      '!uid' => $uid,
    ], WATCHDOG_ERROR);
    return FALSE;
  }
  if (empty($params['type'])) {
    $params['type'] = ZOOMAPI_MEETING_TYPE_DEFAULT;
  }
  if (empty($params['start_time']) || empty($params['duration'])) {
    unset($params['start_time']);
    unset($params['duration']);
    $params['type'] = ZOOMAPI_MEETING_TYPE_INSTANT;
  }
  elseif (is_numeric($params['start_time'])) {

    // If a timezone is provided the format the start timestamp using that
    // timezone. Otherwise use the server timezone.
    // @todo configuration to use host timezone?
    $params['timezone'] = !empty($params['timezone']) ? $params['timezone'] : variable_get('date_default_timezone', @date_default_timezone_get());
    $params['start_time'] = zoomapi_format_timestamp($params['start_time'], $params['timezone']);
  }
  drupal_alter('zoomapi_create_meeting', $params, $context);
  return zoomapi_api_create_meeting($zoom_user_id, $params, $context);
}

/**
 * Create Meeting for Entity.
 */
function zoomapi_create_meeting_for_entity($entity, $entity_type, array $params = [], $uid = 0) {
  if (is_numeric($entity)) {
    $entity = entity_load_single($entity_type, $entity);
  }
  $context = [
    'entity_type' => $entity_type,
    'entity' => $entity,
    'uid' => $uid ?: $entity->uid,
  ];
  if (empty($params['topic'])) {
    $entity_wrapper = entity_metadata_wrapper($entity_type, $entity);
    $params['topic'] = $entity_wrapper
      ->label();
  }
  drupal_alter('zoomapi_create_meeting_for_entity', $params, $context);
  return zoomapi_create_meeting($context['uid'], $params, $context);
}

/**
 * Create Instant Meeting for User.
 */
function zoomapi_create_instant_meeting_for_user($account_uid, array $params = []) {
  $params['type'] = ZOOMAPI_MEETING_TYPE_INSTANT;
  return zoomapi_create_meeting_for_entity($account_uid, 'user', $params);
}

/**
 * Get Meeting.
 */
function zoomapi_get_meeting($meeting_id) {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  return zoomapi_api_get_meeting($meeting_id);
}

/**
 * Get Meeting for Entity.
 *
 * @param string $entity_type
 *   The type (node, user, etc.) of entity.
 * @param int|object $entity_id
 *   The entity (or entity_id) the meeting is being created for.
 *
 * @return array
 *   An array of the meeting setup.
 */
function zoomapi_get_meeting_for_entity($entity_type, $entity_id, $api_refresh = FALSE) {
  if (!is_numeric($entity_id)) {
    list($entity_id, , ) = entity_extract_ids($entity_type, $entity_id);
  }

  // We only expect a single record for any entity_type/entity_id. Just in case
  // that is a false assumption, sort by the created date DESC so we grab the
  // most recent record.
  $data = db_query("\n    SELECT\n      *\n    FROM {zoomapi_meetings_index}\n    WHERE entity_type = :entity_type\n    AND entity_id = :entity_id\n    AND realm = :realm\n    ORDER BY created DESC\n  ", [
    ':entity_type' => $entity_type,
    ':entity_id' => $entity_id,
    ':realm' => zoomapi_realm(),
  ])
    ->fetchAssoc();
  if ($api_refresh && !empty($data['id'])) {
    $data = zoomapi_get_meeting($data['id']);
  }
  return $data;
}

/**
 * Get entity from meeting.
 */
function zoomapi_get_entity_from_meeting($meeting_id) {
  $results = db_query("\n    SELECT\n      entity_type,\n      entity_id\n    FROM {zoomapi_meetings_index}\n    WHERE realm = :realm\n    AND (\n      uuid = :meeting_id\n      OR id = :meeting_id\n    )\n    ORDER BY created DESC\n  ", [
    ':meeting_id' => $meeting_id,
    ':realm' => zoomapi_realm(),
  ])
    ->fetchAssoc();
  return $results ? [
    $results['entity_type'],
    $results['entity_id'],
  ] : [
    '',
    0,
  ];
}

/**
 * Join Meeting.
 *
 * @param array|string $zoom_meeting
 *   The meeting_id or array.
 */
function zoomapi_join_meeting($zoom_meeting) {
  global $user;
  if (is_numeric($zoom_meeting)) {
    $zoom_meeting = zoomapi_get_meeting($zoom_meeting);
  }
  if ($zoom_user_id = zoomapi_get_zoom_user_id($user->uid)) {
    $url = $zoom_user_id && $zoom_user_id == $zoom_meeting['host_id'] ? $zoom_meeting['start_url'] : $zoom_meeting['join_url'];
    watchdog(__FUNCTION__, 'User (uid: !uid) joined meeting "@topic" (id: !meeting_id).', [
      '!uid' => $user->uid,
      '@topic' => $zoom_meeting['topic'],
      '!meeting_id' => $zoom_meeting['id'],
    ], WATCHDOG_INFO);
    drupal_goto($url, [
      'external' => TRUE,
    ]);
  }
  drupal_goto();
}

/**
 * Zoom Meeting Tracker.
 *
 * Track basic meeting information in case a user disconnects and we need to
 * attempt to lookup that meeting info.
 */
function zoomapi_track_meeting(array $zoom_meeting, $entity_type = '', $entity_id = 0) {
  $defaults = [
    'uuid' => '',
    'id' => '',
    'host_id' => '',
    'topic' => '',
    'type' => 0,
    'start_time' => '',
    'duration' => 0,
    'timezone' => '',
    'start_url' => '',
    'join_url' => '',
    'created' => REQUEST_TIME,
    'realm' => zoomapi_realm(),
    'entity_type' => $entity_type,
    'entity_id' => $entity_id,
  ];
  $record = array_merge($defaults, array_intersect_key($zoom_meeting, $defaults));
  db_merge('zoomapi_meetings_index')
    ->key([
    'uuid' => $record['uuid'],
  ])
    ->fields($record)
    ->execute();
}

/**
 * Extract entity type/id from meeting context variable.
 */
function zoomapi_extract_entity_info_from_meeting_context($context) {
  $entity_type = !empty($context['entity_type']) ? $context['entity_type'] : '';
  $entity_id = !empty($context['entity_id']) ? $context['entity_id'] : 0;
  if (!empty($context['entity'])) {
    $entity_type = $entity_type ?: $context['entity']
      ->entityType();
    if (!$entity_id) {
      list($entity_id, , ) = entity_extract_ids($entity_type, $context['entity']);
    }
  }
  return [
    $entity_type,
    $entity_id,
  ];
}

/**
 * Get User Recordings.
 */
function zoomapi_get_user_recordings($account) {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  $uid = is_numeric($account) ? $account : $account->uid;
  $zoom_user_id = zoomapi_get_zoom_user_id($uid);
  if (!$zoom_user_id) {
    watchdog(__FUNCTION__, 'Unable to get recordings for user !uid. Unable to lookup Zoom account ID.', [
      '!uid' => $uid,
    ], WATCHDOG_ERROR);
    return FALSE;
  }
  return zoomapi_api_get_user_recordings($zoom_user_id);
}

/**
 * Get Meeting Recordings.
 *
 * Retrieve the Zoom meeting recordings response.
 */
function zoomapi_get_meeting_recordings($meeting_id, $refresh = FALSE) {
  $zoom_meeting_recordings = [];
  if (!$refresh && ($tracker = zoomapi_get_recording_download_tracking_info($meeting_id))) {
    $zoom_meeting_recordings = !empty($tracker['data']) ? $tracker['data'] : [];
  }
  if ($refresh || empty($zoom_meeting_recordings)) {
    module_load_include('inc', 'zoomapi', 'zoomapi.api');
    $zoom_meeting_recordings = zoomapi_api_get_meeting_recordings($meeting_id);
  }
  return $zoom_meeting_recordings;
}

/**
 * Get Entity Meeting Recordings.
 *
 * Retrieves the Zoom meeting recordings response.
 */
function zoomapi_get_meeting_recordings_for_entity($entity_type, $entity_id) {
  if (!is_numeric($entity_id)) {
    list($entity_id, , ) = entity_extract_ids($entity_type, $entity_id);
  }
  $meeting_info = zoomapi_get_meeting_for_entity($entity_type, $entity_id);
  $recordings = $meeting_info ? zoomapi_get_meeting_recordings($meeting_info['id']) : [];
  return $recordings;
}

/**
 * Download Meeting Recordings.
 *
 * Download the meeting recordings to the specified destination.
 */
function zoomapi_download_meeting_recordings($zoom_meeting_recordings, $destination_directory, $context = []) {
  $filename_prefix = !empty($zoom_meeting_recordings['topic']) ? $zoom_meeting_recordings['topic'] : $zoom_meeting_recordings['id'];
  $files = [];
  drupal_alter('zoomapi_download_meeting_recordings', $zoom_meeting_recordings, $context);
  foreach ($zoom_meeting_recordings['recording_files'] as $zoom_meeting_recording) {
    if ($zoom_meeting_recording['status'] != 'completed') {
      watchdog(__FUNCTION__, 'Unable to download recording !recording_id. The recording has not completed.', [
        '!recording_id' => $zoom_meeting_recording['id'],
      ], WATCHDOG_ERROR);
      $files[$zoom_meeting_recording['id']] = FALSE;
      continue;
    }
    $filename = $filename_prefix . '-' . strtotime($zoom_meeting_recording['recording_start']) . '.' . strtolower($zoom_meeting_recording['file_type']);
    $context['meeting'] = $zoom_meeting_recordings;
    $context['recording'] = $zoom_meeting_recording;
    unset($context['meeting']['recording_files']);
    drupal_alter('zoomapi_meeting_recording_filename', $filename, $context);
    $file = zoomapi_download_recording($zoom_meeting_recording, $destination_directory, $filename, $context);
    $files[$zoom_meeting_recording['id']] = $file;
  }
  return $files;
}

/**
 * Download meeting recordings for entity.
 *
 * Given an entity, lookup any meetings and download any recordings to a
 * specified location.
 */
function zoomapi_download_meeting_recordings_to_entity($entity, $entity_type, $field_name, array $zoom_meeting_recordings = []) {
  if (is_numeric($entity)) {
    $entity = entity_load_single($entity_type, $entity);
  }
  list($entity_id, , ) = entity_extract_ids($entity_type, $entity);
  $destination_directory = zoomapi_get_entity_field_location($entity, $entity_type, $field_name);
  $zoom_meeting_recordings = $zoom_meeting_recordings ?: zoomapi_get_meeting_recordings_for_entity($entity_type, $entity);
  if (!zoomapi_get_recording_download_tracking_info($zoom_meeting_recordings['uuid'])) {
    $destination_info = "{$entity_type}:{$entity_id}:{$field_name}";
    zoomapi_start_recording_download_tracking($zoom_meeting_recordings, $destination_info, 'entity');
  }
  $context = [
    'entity' => $entity,
    'entity_id' => $entity_id,
    'entity_type' => $entity_type,
    'field_name' => $field_name,
  ];
  $files = $zoom_meeting_recordings ? zoomapi_download_meeting_recordings($zoom_meeting_recordings, $destination_directory, $context) : [];
  $success = count($files) == count(array_filter($files));
  zoomapi_update_recording_download_tracking($zoom_meeting_recordings['uuid'], $success);
  zoomapi_attach_recording_files_to_entity_field($entity, $entity_type, $field_name, $files, $success);
  return $success;
}

/**
 * Queue Worker: Download meeting recordings for entity.
 */
function zoomapi_download_meeting_recordings_to_entity_worker($meeting_uuid) {
  $tracker = db_query("\n    SELECT\n      *\n    FROM {zoomapi_recordings_download_tracker}\n    WHERE meeting_uuid = :meeting_uuid\n  ", [
    ':meeting_uuid' => $meeting_uuid,
  ])
    ->fetchObject();
  if ($tracker && !$tracker->success) {
    zoomapi_download_meeting_recordings_from_tracker($tracker);
  }
}

/**
 * Download meeting recordings.
 */
function zoomapi_download_meeting_recordings_from_tracker($tracker) {
  try {
    if ($tracker->destination_type != 'entity') {
      throw new Exception('Meeting is not associated with an entity.');
    }
    list($entity_type, $entity_id, $field_name) = explode(':', $tracker->destination);
    $entity = entity_load_single($entity_type, $entity_id);
    if (!$entity) {
      throw new Exception('Unable to load entity to download meeting recordings.');
    }
    $data = is_array($tracker->data) ? $tracker->data : unserialize($tracker->data);
    return zoomapi_download_meeting_recordings_to_entity($entity, $entity_type, $field_name, $data);
  } catch (\Exception $e) {
    watchdog(__FUNCTION__, 'Unable to download pending meeting recordings for meeting. Error: @e -- Debug: !debug', [
      '@e' => $e
        ->getMessage(),
      '!debug' => '<pre>' . print_r($tracker, TRUE) . '</pre>',
    ], WATCHDOG_ERROR);
    return FALSE;
  }
}

/**
 * Attach recording files to entity field.
 */
function zoomapi_attach_recording_files_to_entity_field($entity, $entity_type, $field_name, array $files, $replace_all_files = FALSE) {
  if (is_numeric($entity)) {
    $entity = entity_load_single($entity_type, $entity);
  }
  $context = [
    'entity_type' => $entity_type,
    'field_name' => $field_name,
    'replace' => $replace_all_files,
  ];
  $files = array_values(array_filter($files));
  drupal_alter('zoomapi_attach_recordings_to_entity', $entity, $files, $context);
  $field_files = !empty($entity->{$field_name}[LANGUAGE_NONE]) ? $entity->{$field_name}[LANGUAGE_NONE] : [];
  $field_files = $replace_all_files ? $files : array_merge($field_files, $files);
  $entity->{$field_name}[LANGUAGE_NONE] = $field_files;
  entity_save($entity_type, $entity);
  return $entity;
}

/**
 * Set recording download tracking info.
 */
function zoomapi_start_recording_download_tracking(array $meeting_recordings, $destination, $destination_type = 'entity') {
  $record = [
    'meeting_uuid' => $meeting_recordings['uuid'],
    'meeting_id' => $meeting_recordings['id'],
    'destination_type' => $destination_type,
    'destination' => $destination,
    'success' => 0,
    'attempts' => 0,
    'data' => is_array($meeting_recordings) ? serialize($meeting_recordings) : $meeting_recordings,
    'realm' => zoomapi_realm(),
    'created' => REQUEST_TIME,
    'changed' => REQUEST_TIME,
  ];
  db_merge('zoomapi_recordings_download_tracker')
    ->key([
    'meeting_uuid' => $meeting_recordings['uuid'],
  ])
    ->fields($record)
    ->execute();
  return $record;
}

/**
 * Get recording download tracking info.
 */
function zoomapi_get_recording_download_tracking_info($meeting_uuid) {
  $record = db_query("\n    SELECT\n      *\n    FROM {zoomapi_recordings_download_tracker}\n    WHERE realm = :realm\n    AND (\n      meeting_uuid = :meeting_uuid\n      OR meeting_id = :meeting_uuid\n    )\n  ", [
    ':realm' => zoomapi_realm(),
    ':meeting_uuid' => $meeting_uuid,
  ])
    ->fetchAssoc();
  if (!empty($record)) {
    $data = is_string($record['data']) ? unserialize($record['data']) : $record['data'];
    if (zoomapi_validate_recording_tracking_info($data)) {
      $record['data'] = $data;
    }
  }
  return $record;
}

/**
 * Validate recording data.
 */
function zoomapi_validate_recording_tracking_info($info) {

  // It appears that around April 11th, 2020 the recording data provided in
  // webhooks is incorrect, causing automated downloads to fail. This also
  // happens to be around the time Zoom faced many of the security issues
  // during the Coronavirus outbreak... hence Zoom support has been
  // unreachable. To work around this, some logic checks are put in place
  // to attempt to validate the recording data.
  if (empty($info['total_size'])) {
    return FALSE;
  }
  if ($info['recording_count'] !== count($info['recording_files'])) {
    return FALSE;
  }
  foreach ($info['recording_files'] as $recording_file) {
    if (empty($recording_file['file_type'])) {
      return FALSE;
    }
    if (empty($recording_file['file_size'])) {
      return FALSE;
    }
    if ($recording_file['status'] !== 'completed') {
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Get unsuccessful recording downloads meeting uuid.
 */
function zoomapi_get_unsuccessful_recording_download_uuids($max_attempts = NULL, $how_far_back = NULL) {
  $max_attempts = $max_attempts ?: variable_get('zoomapi_recordings_download_max_attempts', 3);
  $how_far_back = $how_far_back ?: variable_get('zoomapi_recordings_download_how_far_back', '-30 days');
  $uuids = db_query("\n    SELECT\n      meeting_uuid\n    FROM {zoomapi_recordings_download_tracker}\n    WHERE realm = :realm\n    AND success = :nosuccess\n    AND :max_attempts > attempts\n    AND created >= :created_after\n  ", [
    ':realm' => zoomapi_realm(),
    ':nosuccess' => 0,
    ':max_attempts' => $max_attempts,
    ':created_after' => strtotime($how_far_back),
  ])
    ->fetchCol();
  return $uuids;
}

/**
 * Get unsuccessful recording downloads.
 */
function zoomapi_get_unsuccessful_recording_download_tracking_info($set_size = NULL, $max_attempts = NULL, $how_far_back = NULL) {
  $set_size = $set_size ?: variable_get('zoomapi_recordings_download_cron_set_size', 10);
  $max_attempts = $max_attempts ?: variable_get('zoomapi_recordings_download_max_attempts', 3);
  $how_far_back = $how_far_back ?: variable_get('zoomapi_recordings_download_how_far_back', '-30 days');
  $recordings = db_query_range("\n    SELECT\n      *\n    FROM {zoomapi_recordings_download_tracker}\n    WHERE realm = :realm\n    AND success = :nosuccess\n    AND :max_attempts > attempts\n    AND created >= :created_after\n  ", 0, $set_size, [
    ':realm' => zoomapi_realm(),
    ':nosuccess' => 0,
    ':max_attempts' => $max_attempts,
    ':created_after' => strtotime($how_far_back),
  ])
    ->fetchAllAssoc('meeting_uuid');
  foreach ($recordings as &$recording) {
    $data = is_string($recording->data) ? unserialize($recording->data) : $recording->data;
    if (zoomapi_validate_recording_tracking_info($data)) {
      $recording->data = $data;
    }
  }
  return $recordings;
}

/**
 * Update recordings download tracking info.
 */
function zoomapi_update_recording_download_tracking($meeting_uuid, $success) {
  if ($record = zoomapi_get_recording_download_tracking_info($meeting_uuid)) {
    $record['attempts']++;
    $record['success'] = (int) $success;
    $record['changed'] = REQUEST_TIME;
    $record['data'] = is_array($record['data']) ? serialize($record['data']) : $record['data'];
    db_merge('zoomapi_recordings_download_tracker')
      ->key([
      'meeting_uuid' => $meeting_uuid,
    ])
      ->fields($record)
      ->execute();
    return $record;
  }
  return [];
}

/**
 * Get file filed location for entity field.
 */
function zoomapi_get_entity_field_location($entity, $entity_type, $field_name) {
  $field_info = field_info_field($field_name);
  $uri_scheme = $field_info['settings']['uri_scheme'];
  if (is_numeric($entity)) {
    $entity = entity_load_single($entity_type, $entity);
  }
  list(, , $bundle) = entity_extract_ids($entity_type, $entity);
  $field_instance_info = field_info_instance($entity_type, $field_name, $bundle);
  $file_directory = $field_instance_info['settings']['file_directory'];
  if (module_exists('filefield_paths') && !empty($field_instance_info['settings']['filefield_paths'])) {
    $settings = $field_instance_info['settings']['filefield_paths'];
    $settings['file_path']['options']['context'] = 'file_path';
    $token_data = [
      $entity_type => $entity,
    ];
    $file_directory = filefield_paths_process_string($settings['file_path']['value'], $token_data, $settings['file_path']['options']);
  }
  $destination_directory = "{$uri_scheme}://{$file_directory}";
  return $destination_directory;
}

/**
 * Download Recording.
 *
 * Downloads a zoom recording to the specified destination / filename.
 *
 * @see filefield_source_remote_value()
 */
function zoomapi_download_recording($zoom_meeting_recording, $destination_directory, $filename = '', $context = []) {
  module_load_include('inc', 'zoomapi', 'zoomapi.api');
  return zoomapi_api_download_recording($zoom_meeting_recording, $destination_directory, $filename, $context);
}

/**
 * Clean up the file name, munging extensions and transliterating.
 *
 * @param string $filepath
 *   A string containing a file name or full path. Only the file name will
 *   actually be modified.
 *
 * @return string
 *   A file path with a cleaned-up file name.
 *
 * @see filefield_sources_clean_filename()
 */
function zoomapi_clean_filename($filepath, $extensions = '') {
  $filename = basename($filepath);
  if (module_exists('transliteration')) {
    module_load_include('inc', 'transliteration');
    $langcode = NULL;
    if (!empty($_POST['language'])) {
      $languages = language_list();
      $langcode = isset($languages[$_POST['language']]) ? $_POST['language'] : NULL;
    }
    $filename = transliteration_clean_filename($filename, $langcode);
  }

  // Because this transfer mechanism does not use file_save_upload(), we need
  // to manually munge the filename to prevent dangerous extensions.
  // See file_save_upload().
  if (empty($extensions)) {
    $extensions = 'mp4 m4a';
  }
  $filename = file_munge_filename($filename, $extensions);
  $directory = drupal_dirname($filepath);
  return ($directory != '.' ? $directory . '/' : '') . $filename;
}

/**
 * The cURL write function to save the file to disk.
 */
function zoomapi_download_recording_curl_write(&$ch, $data) {
  $options = zoomapi_get_transfer_options();
  $data_length = 0;
  if ($fp = @fopen($options['filepath'], 'a')) {
    fwrite($fp, $data);
    fclose($fp);
    $data_length = strlen($data);
  }
  return $data_length;
}

/**
 * Set a transfer key that can be retreived by the progress function.
 */
function zoomapi_set_transfer_options($options = NULL) {
  static $current = FALSE;
  if (isset($options)) {
    $current = $options;
  }
  return $current;
}

/**
 * Get a transfer key that can be retrieved by the progress function.
 */
function zoomapi_get_transfer_options() {
  return zoomapi_set_transfer_options();
}

/**
 * Save a file into the database after validating it.
 */
function zoomapi_save_file($filepath, $destination_directory, $replace = FILE_EXISTS_REPLACE) {

  // Begin building file object.
  $file = new stdClass();
  $file->status = 1;
  $file->display = 1;
  $file->filename = trim(basename($filepath), '.');
  $file->uri = $filepath;
  $file->filemime = file_get_mimetype($file->filename);
  $file->filesize = filesize($filepath);
  $extensions = 'mp4 mpa';

  // Munge the filename to protect against possible malicious extension hiding
  // within an unknown file type (ie: filename.html.foo).
  $file->filename = file_munge_filename($file->filename, $extensions);

  // Assert that the destination contains a valid stream.
  $destination_scheme = file_uri_scheme($destination_directory);
  if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) {
    watchdog(__FUNCTION__, 'The recording could not be saved, because the destination %destination is invalid.', [
      '%destination' => $destination_directory,
    ]);
    return FALSE;
  }

  // A URI may already have a trailing slash or look like "public://".
  if (substr($destination_directory, -1) != '/') {
    $destination_directory .= '/';
  }

  // Ensure the destination is writable.
  file_prepare_directory($destination_directory, FILE_CREATE_DIRECTORY);
  $file->destination = file_destination($destination_directory . $file->filename, $replace);

  // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary
  // directory. This overcomes open_basedir restrictions for future file
  // operations.
  $file->uri = $file->destination;
  if (!file_unmanaged_copy($filepath, $file->uri, $replace)) {
    watchdog(__FUNCTION__, 'Recording error. Could not move recording file %file to destination %destination.', [
      '%file' => $file->filename,
      '%destination' => $file->uri,
    ]);
    return FALSE;
  }

  // Set the permissions on the new file.
  drupal_chmod($file->uri);

  // If we are replacing an existing file re-use its database record.
  if ($replace == FILE_EXISTS_REPLACE) {
    $existing_files = file_load_multiple(array(), [
      'uri' => $file->uri,
    ]);
    if (count($existing_files)) {
      $existing = reset($existing_files);
      $file->fid = $existing->fid;
    }
  }

  // If we made it this far it's safe to record this file in the database.
  if ($file = file_save($file)) {
    return $file;
  }
  return FALSE;
}

/**
 * Format datetime helper.
 */
function zoomapi_format_timestamp($timestamp, $timezone = '') {
  $format = $timezone ? ZOOMAPI_MEETING_TIME_FORMAT_LOCAL : ZOOMAPI_MEETING_TIME_FORMAT_DEFAULT;
  $timezone = $timezone ?: ZOOMAPI_MEETING_TIME_FORMAT_TZ;
  return format_date($timestamp, 'custom', $format, $timezone);
}

/**
 * Get realm.
 */
function zoomapi_realm($url = '') {
  global $base_url;
  $realm = variable_get('zoomapi_realm', '');
  if (!$realm || $url) {
    $url = $url ?: $base_url;
    $realm = explode('.', parse_url($url)['host'])[0];
  }
  return $realm;
}

/**
 * Format datetime helper.
 */
function zoomapi_format_timestamp_local_server($timestamp) {
  $timezone = variable_get('date_default_timezone', @date_default_timezone_get());
  return zoomapi_format_timestamp($timestamp, $timezone);
}

/**
 * Format datetime helper.
 */
function zoomapi_format_timestamp_local_user($timestamp, $account = NULL) {
  $timezone = zoomapi_get_account_timezone($account);
  return zoomapi_format_timestamp($timestamp, $timezone);
}

/**
 * Get account timezone.
 */
function zoomapi_get_account_timezone($account) {
  $timezone = drupal_get_user_timezone();
  if ($account) {
    $account = is_numeric($account) ? user_load($account) : $account;
    if (!empty($account->timezone)) {
      $timezone = $account->timezone;
    }
  }
  return $timezone;
}

/**
 * Get webhook url part.
 */
function _zoomapi_get_webhook_url_part($url) {
  $prefix = 'zoomapi/webhook';
  $parsed_url = parse_url($url);
  $path = trim($parsed_url['path']);
  $part = substr($path, 0, strlen($prefix)) == $prefix ? substr($url, strlen($prefix)) : '';
  return $part;
}

Functions

Namesort descending Description
zoomapi_attach_recording_files_to_entity_field Attach recording files to entity field.
zoomapi_clean_filename Clean up the file name, munging extensions and transliterating.
zoomapi_client Get ZoomAPI client.
zoomapi_create_instant_meeting_for_user Create Instant Meeting for User.
zoomapi_create_meeting Create Zoom Meeting.
zoomapi_create_meeting_for_entity Create Meeting for Entity.
zoomapi_create_user Create Zoom user.
zoomapi_cron Implements hook_cron().
zoomapi_cron_queue_info Implements hook_cron_queue_info().
zoomapi_download_meeting_recordings Download Meeting Recordings.
zoomapi_download_meeting_recordings_from_tracker Download meeting recordings.
zoomapi_download_meeting_recordings_to_entity Download meeting recordings for entity.
zoomapi_download_meeting_recordings_to_entity_worker Queue Worker: Download meeting recordings for entity.
zoomapi_download_recording Download Recording.
zoomapi_download_recording_curl_write The cURL write function to save the file to disk.
zoomapi_extract_entity_info_from_meeting_context Extract entity type/id from meeting context variable.
zoomapi_format_timestamp Format datetime helper.
zoomapi_format_timestamp_local_server Format datetime helper.
zoomapi_format_timestamp_local_user Format datetime helper.
zoomapi_generate_user_email Generate zoom user email.
zoomapi_get_account_timezone Get account timezone.
zoomapi_get_entity_field_location Get file filed location for entity field.
zoomapi_get_entity_from_meeting Get entity from meeting.
zoomapi_get_meeting Get Meeting.
zoomapi_get_meeting_for_entity Get Meeting for Entity.
zoomapi_get_meeting_recordings Get Meeting Recordings.
zoomapi_get_meeting_recordings_for_entity Get Entity Meeting Recordings.
zoomapi_get_recording_download_tracking_info Get recording download tracking info.
zoomapi_get_transfer_options Get a transfer key that can be retrieved by the progress function.
zoomapi_get_unsuccessful_recording_download_tracking_info Get unsuccessful recording downloads.
zoomapi_get_unsuccessful_recording_download_uuids Get unsuccessful recording downloads meeting uuid.
zoomapi_get_user Get Zoom user.
zoomapi_get_users Get Zoom users.
zoomapi_get_user_from_zoom_userid Get Drupal uid from Zoom user ID.
zoomapi_get_user_recordings Get User Recordings.
zoomapi_get_user_settings Get Zoom User settings.
zoomapi_get_zoom_users_tracker_info Get Zoom Users Tracker Info.
zoomapi_get_zoom_user_email Get Zoom User Email.
zoomapi_get_zoom_user_id Get Zoom User ID.
zoomapi_get_zoom_user_tracker_info Get Zoom User Tracker Info.
zoomapi_join_meeting Join Meeting.
zoomapi_menu Implements hook_menu().
zoomapi_permission Implements hook_permission().
zoomapi_realm Get realm.
zoomapi_save_file Save a file into the database after validating it.
zoomapi_set_transfer_options Set a transfer key that can be retreived by the progress function.
zoomapi_start_recording_download_tracking Set recording download tracking info.
zoomapi_track_meeting Zoom Meeting Tracker.
zoomapi_track_user Track Drupal / Zoom user account relationship.
zoomapi_update_recording_download_tracking Update recordings download tracking info.
zoomapi_update_user Update Zoom user.
zoomapi_update_user_email Update Zoom user email.
zoomapi_update_user_settings Update Zoom User settings.
zoomapi_user_email_exists Check if email in use.
zoomapi_user_insert Implements hook_user_insert().
zoomapi_validate_recording_tracking_info Validate recording data.
zoomapi_webhooks_access Access callback: Zoom API Webhooks.
zoomapi_zoomapi_user_create Implements hook_zoomapi_user_create().
_zoomapi_get_webhook_url_part Get webhook url part.

Constants

Classes

Namesort descending Description
ZoomAPISystemQueue Extend SystemQueue making each item unique.