Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace mod_bigbluebuttonbn;
  18  
  19  use cache;
  20  use cache_store;
  21  use context_course;
  22  use core_tag_tag;
  23  use Exception;
  24  use Firebase\JWT\Key;
  25  use mod_bigbluebuttonbn\local\config;
  26  use mod_bigbluebuttonbn\local\exceptions\bigbluebutton_exception;
  27  use mod_bigbluebuttonbn\local\exceptions\meeting_join_exception;
  28  use mod_bigbluebuttonbn\local\helpers\roles;
  29  use mod_bigbluebuttonbn\local\proxy\bigbluebutton_proxy;
  30  use stdClass;
  31  
  32  /**
  33   * Class to describe a BBB Meeting.
  34   *
  35   * @package   mod_bigbluebuttonbn
  36   * @copyright 2021 Andrew Lyons <andrew@nicols.co.uk>
  37   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class meeting {
  40  
  41      /** @var instance The bbb instance */
  42      protected $instance;
  43  
  44      /** @var stdClass Info about the meeting */
  45      protected $meetinginfo = null;
  46  
  47      /**
  48       * Constructor for the meeting object.
  49       *
  50       * @param instance $instance
  51       */
  52      public function __construct(instance $instance) {
  53          $this->instance = $instance;
  54      }
  55  
  56      /**
  57       * Helper to join a meeting.
  58       *
  59       *
  60       * It will create the meeting if not already created.
  61       *
  62       * @param instance $instance
  63       * @param int $origin
  64       * @return string
  65       * @throws meeting_join_exception this is sent if we cannot join (meeting full, user needs to wait...)
  66       */
  67      public static function join_meeting(instance $instance, $origin = logger::ORIGIN_BASE): string {
  68          // See if the session is in progress.
  69          $meeting = new meeting($instance);
  70          // As the meeting doesn't exist, try to create it.
  71          if (empty($meeting->get_meeting_info(true)->createtime)) {
  72              $meeting->create_meeting();
  73          }
  74          return $meeting->join($origin);
  75      }
  76  
  77      /**
  78       * Get currently stored meeting info
  79       *
  80       * @return stdClass
  81       */
  82      public function get_meeting_info() {
  83          if (!$this->meetinginfo) {
  84              $this->meetinginfo = $this->do_get_meeting_info();
  85          }
  86          return $this->meetinginfo;
  87      }
  88  
  89      /**
  90       * Return meeting information for the specified instance.
  91       *
  92       * @param instance $instance
  93       * @param bool $updatecache Whether to update the cache when fetching the information
  94       * @return stdClass
  95       */
  96      public static function get_meeting_info_for_instance(instance $instance, bool $updatecache = false): stdClass {
  97          $meeting = new self($instance);
  98          return $meeting->do_get_meeting_info($updatecache);
  99      }
 100  
 101      /**
 102       * Helper function returns a sha1 encoded string that is unique and will be used as a seed for meetingid.
 103       *
 104       * @return string
 105       */
 106      public static function get_unique_meetingid_seed() {
 107          global $DB;
 108          do {
 109              $encodedseed = sha1(plugin::random_password(12));
 110              $meetingid = (string) $DB->get_field('bigbluebuttonbn', 'meetingid', ['meetingid' => $encodedseed]);
 111          } while ($meetingid == $encodedseed);
 112          return $encodedseed;
 113      }
 114  
 115      /**
 116       * Is meeting running ?
 117       *
 118       * @return bool
 119       */
 120      public function is_running() {
 121          return $this->get_meeting_info()->statusrunning ?? false;
 122      }
 123  
 124      /**
 125       * Force update the meeting in cache.
 126       */
 127      public function update_cache() {
 128          $this->meetinginfo = $this->do_get_meeting_info(true);
 129      }
 130  
 131      /**
 132       * Get meeting attendees
 133       *
 134       * @return array[]
 135       */
 136      public function get_attendees(): array {
 137          return $this->get_meeting_info()->attendees ?? [];
 138      }
 139  
 140      /**
 141       * Can the meeting be joined ?
 142       *
 143       * @return bool
 144       */
 145      public function can_join() {
 146          return $this->get_meeting_info()->canjoin;
 147      }
 148  
 149      /**
 150       * Total number of moderators and viewers.
 151       *
 152       * @return int
 153       */
 154      public function get_participant_count() {
 155          return $this->get_meeting_info()->totalusercount;
 156      }
 157  
 158      /**
 159       * Creates a bigbluebutton meeting, send the message to BBB and returns the response in an array.
 160       *
 161       * @return array
 162       */
 163      public function create_meeting() {
 164          $data = $this->create_meeting_data();
 165          $metadata = $this->create_meeting_metadata();
 166          $presentation = $this->instance->get_presentation_for_bigbluebutton_upload(); // The URL must contain nonce.
 167          $presentationname = $presentation['name'] ?? null;
 168          $presentationurl = $presentation['url'] ?? null;
 169          $response = bigbluebutton_proxy::create_meeting($data, $metadata, $presentationname, $presentationurl);
 170          // New recording management: Insert a recordingID that corresponds to the meeting created.
 171          if ($this->instance->is_recorded()) {
 172              $recording = new recording(0, (object) [
 173                  'courseid' => $this->instance->get_course_id(),
 174                  'bigbluebuttonbnid' => $this->instance->get_instance_id(),
 175                  'recordingid' => $response['internalMeetingID'],
 176                  'groupid' => $this->instance->get_group_id()]
 177              );
 178              $recording->create();
 179          }
 180          return $response;
 181      }
 182  
 183      /**
 184       * Send an end meeting message to BBB server
 185       */
 186      public function end_meeting() {
 187          bigbluebutton_proxy::end_meeting($this->instance->get_meeting_id(), $this->instance->get_moderator_password());
 188      }
 189  
 190      /**
 191       * Get meeting join URL
 192       *
 193       * @return string
 194       */
 195      public function get_join_url(): string {
 196          return bigbluebutton_proxy::get_join_url(
 197              $this->instance->get_meeting_id(),
 198              $this->instance->get_user_fullname(),
 199              $this->instance->get_current_user_password(),
 200              $this->instance->get_logout_url()->out(false),
 201              $this->instance->get_current_user_role(),
 202              null,
 203              $this->instance->get_user_id(),
 204              $this->get_meeting_info()->createtime
 205          );
 206      }
 207  
 208      /**
 209       * Get meeting join URL for guest
 210       *
 211       * @param string $fullname
 212       * @return string
 213       */
 214      public function get_guest_join_url(string $fullname): string {
 215          return bigbluebutton_proxy::get_join_url(
 216              $this->instance->get_meeting_id(),
 217              $fullname,
 218              $this->instance->get_current_user_password(),
 219              $this->instance->get_guest_access_url()->out(false),
 220              $this->instance->get_current_user_role(),
 221              null,
 222              0,
 223              $this->get_meeting_info()->createtime
 224          );
 225      }
 226  
 227  
 228      /**
 229       * Return meeting information for this meeting.
 230       *
 231       * @param bool $updatecache Whether to update the cache when fetching the information
 232       * @return stdClass
 233       */
 234      protected function do_get_meeting_info(bool $updatecache = false): stdClass {
 235          $instance = $this->instance;
 236          $meetinginfo = (object) [
 237              'instanceid' => $instance->get_instance_id(),
 238              'bigbluebuttonbnid' => $instance->get_instance_id(),
 239              'groupid' => $instance->get_group_id(),
 240              'meetingid' => $instance->get_meeting_id(),
 241              'cmid' => $instance->get_cm_id(),
 242              'ismoderator' => $instance->is_moderator(),
 243              'joinurl' => $instance->get_join_url()->out(),
 244              'userlimit' => $instance->get_user_limit(),
 245              'presentations' => [],
 246          ];
 247          if ($instance->get_instance_var('openingtime')) {
 248              $meetinginfo->openingtime = intval($instance->get_instance_var('openingtime'));
 249          }
 250          if ($instance->get_instance_var('closingtime')) {
 251              $meetinginfo->closingtime = intval($instance->get_instance_var('closingtime'));
 252          }
 253          $activitystatus = bigbluebutton_proxy::view_get_activity_status($instance);
 254          // This might raise an exception if info cannot be retrieved.
 255          // But this might be totally fine as the meeting is maybe not yet created on BBB side.
 256          $totalusercount = 0;
 257          // This is the default value for any meeting that has not been created.
 258          $meetinginfo->statusrunning = false;
 259          $meetinginfo->createtime = null;
 260  
 261          $info = self::retrieve_cached_meeting_info($this->instance->get_meeting_id(), $updatecache);
 262          if (!empty($info)) {
 263              $meetinginfo->statusrunning = $info['running'] === 'true';
 264              $meetinginfo->createtime = $info['createTime'] ?? null;
 265              $totalusercount = isset($info['participantCount']) ? $info['participantCount'] : 0;
 266          }
 267  
 268          $meetinginfo->statusclosed = $activitystatus === 'ended';
 269          $meetinginfo->statusopen = !$meetinginfo->statusrunning && $activitystatus === 'open';
 270          $meetinginfo->totalusercount = $totalusercount;
 271  
 272          $canjoin = !$instance->user_must_wait_to_join() || $meetinginfo->statusrunning;
 273          // Limit has not been reached.
 274          $canjoin = $canjoin && (!$instance->has_user_limit_been_reached($totalusercount));
 275          // User should only join during scheduled session start and end time, if defined.
 276          $canjoin = $canjoin && ($instance->is_currently_open());
 277          // Double check that the user has the capabilities to join.
 278          $canjoin = $canjoin && $instance->can_join();
 279          $meetinginfo->canjoin = $canjoin;
 280  
 281          // If user is administrator, moderator or if is viewer and no waiting is required, join allowed.
 282          if ($meetinginfo->statusrunning) {
 283              $meetinginfo->startedat = floor(intval($info['startTime']) / 1000); // Milliseconds.
 284              $meetinginfo->moderatorcount = $info['moderatorCount'];
 285              $meetinginfo->moderatorplural = $info['moderatorCount'] > 1;
 286              $meetinginfo->participantcount = $totalusercount - $meetinginfo->moderatorcount;
 287              $meetinginfo->participantplural = $meetinginfo->participantcount > 1;
 288          }
 289          $meetinginfo->statusmessage = $this->get_status_message($meetinginfo, $instance);
 290  
 291          $presentation = $instance->get_presentation(); // This is for internal use.
 292          if (!empty($presentation)) {
 293              $meetinginfo->presentations[] = $presentation;
 294          }
 295          $meetinginfo->attendees = [];
 296          if (!empty($info['attendees'])) {
 297              // Ensure each returned attendee is cast to an array, rather than a simpleXML object.
 298              foreach ($info['attendees'] as $attendee) {
 299                  $meetinginfo->attendees[] = (array) $attendee;
 300              }
 301          }
 302          $meetinginfo->guestaccessenabled = $instance->is_guest_allowed();
 303          if ($meetinginfo->guestaccessenabled && $instance->is_moderator()) {
 304              $meetinginfo->guestjoinurl = $instance->get_guest_access_url()->out();
 305              $meetinginfo->guestpassword = $instance->get_guest_access_password();
 306          }
 307  
 308          $meetinginfo->features = $instance->get_enabled_features();
 309          return $meetinginfo;
 310      }
 311  
 312      /**
 313       * Deduce status message from the current meeting info and the instance
 314       *
 315       * Returns the human-readable message depending on if the user must wait to join, the meeting has not
 316       * yet started ...
 317       * @param object $meetinginfo
 318       * @param instance $instance
 319       * @return string
 320       */
 321      protected function get_status_message(object $meetinginfo, instance $instance): string {
 322          if ($instance->has_user_limit_been_reached($meetinginfo->totalusercount)) {
 323              return get_string('view_message_conference_user_limit_reached', 'bigbluebuttonbn');
 324          }
 325          if ($meetinginfo->statusrunning) {
 326              return get_string('view_message_conference_in_progress', 'bigbluebuttonbn');
 327          }
 328          if ($instance->user_must_wait_to_join() && !$instance->user_can_force_join()) {
 329              return get_string('view_message_conference_wait_for_moderator', 'bigbluebuttonbn');
 330          }
 331          if ($instance->before_start_time()) {
 332              return get_string('view_message_conference_not_started', 'bigbluebuttonbn');
 333          }
 334          if ($instance->has_ended()) {
 335              return get_string('view_message_conference_has_ended', 'bigbluebuttonbn');
 336          }
 337          return get_string('view_message_conference_room_ready', 'bigbluebuttonbn');
 338      }
 339  
 340      /**
 341       * Gets a meeting info object cached or fetched from the live session.
 342       *
 343       * @param string $meetingid
 344       * @param bool $updatecache
 345       *
 346       * @return array
 347       */
 348      protected static function retrieve_cached_meeting_info($meetingid, $updatecache = false) {
 349          $cachettl = (int) config::get('waitformoderator_cache_ttl');
 350          $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'mod_bigbluebuttonbn', 'meetings_cache');
 351          $result = $cache->get($meetingid);
 352          $now = time();
 353          if (!$updatecache && !empty($result) && $now < ($result['creation_time'] + $cachettl)) {
 354              // Use the value in the cache.
 355              return (array) json_decode($result['meeting_info']);
 356          }
 357          // We set the cache to an empty value so then if get_meeting_info raises an exception we still have the
 358          // info about the last creation_time, so we don't ask the server again for a bit.
 359          $defaultcacheinfo = ['creation_time' => time(), 'meeting_info' => '[]'];
 360          // Pings again and refreshes the cache.
 361          try {
 362              $meetinginfo = bigbluebutton_proxy::get_meeting_info($meetingid);
 363              $cache->set($meetingid, ['creation_time' => time(), 'meeting_info' => json_encode($meetinginfo)]);
 364          } catch (bigbluebutton_exception $e) {
 365              // The meeting is not created on BBB side, so we set the value in the cache so we don't poll again
 366              // and return an empty array.
 367              $cache->set($meetingid, $defaultcacheinfo);
 368              return [];
 369          }
 370          return $meetinginfo;
 371      }
 372  
 373      /**
 374       * Conversion between form settings and lockSettings as set in BBB API.
 375       */
 376      const LOCK_SETTINGS_MEETING_DATA = [
 377          'disablecam' => 'lockSettingsDisableCam',
 378          'disablemic' => 'lockSettingsDisableMic',
 379          'disableprivatechat' => 'lockSettingsDisablePrivateChat',
 380          'disablepublicchat' => 'lockSettingsDisablePublicChat',
 381          'disablenote' => 'lockSettingsDisableNote',
 382          'hideuserlist' => 'lockSettingsHideUserList'
 383      ];
 384      /**
 385       * Helper to prepare data used for create meeting.
 386       * @todo moderatorPW and attendeePW will be removed from create after release of BBB v2.6.
 387       *
 388       * @return array
 389       */
 390      protected function create_meeting_data() {
 391          $data = ['meetingID' => $this->instance->get_meeting_id(),
 392              'name' => \mod_bigbluebuttonbn\plugin::html2text($this->instance->get_meeting_name(), 64),
 393              'attendeePW' => $this->instance->get_viewer_password(),
 394              'moderatorPW' => $this->instance->get_moderator_password(),
 395              'logoutURL' => $this->instance->get_logout_url()->out(false),
 396          ];
 397          $data['record'] = $this->instance->should_record() ? 'true' : 'false';
 398          // Check if auto_start_record is enable.
 399          if ($data['record'] == 'true' && $this->instance->should_record_from_start()) {
 400              $data['autoStartRecording'] = 'true';
 401          }
 402          // Check if hide_record_button is enable.
 403          if (!$this->instance->should_show_recording_button()) {
 404              $data['allowStartStopRecording'] = 'false';
 405          }
 406          $data['welcome'] = trim($this->instance->get_welcome_message());
 407          $voicebridge = intval($this->instance->get_voice_bridge());
 408          if ($voicebridge > 0 && $voicebridge < 79999) {
 409              $data['voiceBridge'] = $voicebridge;
 410          }
 411          $maxparticipants = intval($this->instance->get_user_limit());
 412          if ($maxparticipants > 0) {
 413              $data['maxParticipants'] = $maxparticipants;
 414          }
 415          if ($this->instance->get_mute_on_start()) {
 416              $data['muteOnStart'] = 'true';
 417          }
 418          // Here a bit of a change compared to the API default behaviour: we should not allow guest to join
 419          // a meeting managed by Moodle by default.
 420          if ($this->instance->is_guest_allowed()) {
 421              $data['guestPolicy'] = $this->instance->is_moderator_approval_required() ? 'ASK_MODERATOR' : 'ALWAYS_ACCEPT';
 422          }
 423          // Locks settings.
 424          foreach (self::LOCK_SETTINGS_MEETING_DATA as $instancevarname => $lockname) {
 425              $instancevar = $this->instance->get_instance_var($instancevarname);
 426              if (!is_null($instancevar)) {
 427                  $data[$lockname] = $instancevar ? 'true' : 'false';
 428                  if ($instancevar) {
 429                      $data['lockSettingsLockOnJoin'] = 'true'; // This will be locked whenever one settings is locked.
 430                  }
 431              }
 432          }
 433          return $data;
 434      }
 435  
 436      /**
 437       * Helper for preparing metadata used while creating the meeting.
 438       *
 439       * @return array
 440       */
 441      protected function create_meeting_metadata() {
 442          global $USER;
 443          // Create standard metadata.
 444          $origindata = $this->instance->get_origin_data();
 445          $metadata = [
 446              'bbb-origin' => $origindata->origin,
 447              'bbb-origin-version' => $origindata->originVersion,
 448              'bbb-origin-server-name' => $origindata->originServerName,
 449              'bbb-origin-server-common-name' => $origindata->originServerCommonName,
 450              'bbb-origin-tag' => $origindata->originTag,
 451              'bbb-context' => $this->instance->get_course()->fullname,
 452              'bbb-context-id' => $this->instance->get_course_id(),
 453              'bbb-context-name' => trim(html_to_text($this->instance->get_course()->fullname, 0)),
 454              'bbb-context-label' => trim(html_to_text($this->instance->get_course()->shortname, 0)),
 455              'bbb-recording-name' => plugin::html2text($this->instance->get_meeting_name(), 64),
 456              'bbb-recording-description' => plugin::html2text($this->instance->get_meeting_description(),
 457                  64),
 458              'bbb-recording-tags' =>
 459                  implode(',', core_tag_tag::get_item_tags_array('core',
 460                      'course_modules', $this->instance->get_cm_id())), // Same as $id.
 461              'bbb-meeting-size-hint' =>
 462                  count_enrolled_users(context_course::instance($this->instance->get_course_id()),
 463                      '', $this->instance->get_group_id()),
 464          ];
 465          // Special metadata for recording processing.
 466          if ((boolean) config::get('recordingstatus_enabled')) {
 467              $metadata["bn-recording-status"] = json_encode(
 468                  [
 469                      'email' => ['"' . fullname($USER) . '" <' . $USER->email . '>'],
 470                      'context' => $this->instance->get_view_url(),
 471                  ]
 472              );
 473          }
 474          if ((boolean) config::get('recordingready_enabled')) {
 475              $metadata['bn-recording-ready-url'] = $this->instance->get_record_ready_url()->out(false);
 476          }
 477          if ((boolean) config::get('meetingevents_enabled')) {
 478              $metadata['analytics-callback-url'] = $this->instance->get_meeting_event_notification_url()->out(false);
 479          }
 480          return $metadata;
 481      }
 482  
 483      /**
 484       * Helper for responding when storing live meeting events is requested.
 485       *
 486       * The callback with a POST request includes:
 487       *  - Authentication: Bearer <A JWT token containing {"exp":<TIMESTAMP>} encoded with HS512>
 488       *  - Content Type: application/json
 489       *  - Body: <A JSON Object>
 490       *
 491       * @param instance $instance
 492       * @param object $data
 493       * @return string
 494       */
 495      public static function meeting_events(instance $instance, object $data):  string {
 496          $bigbluebuttonbn = $instance->get_instance_data();
 497          // Validate that the bigbluebuttonbn activity corresponds to the meeting_id received.
 498          $meetingidelements = explode('[', $data->{'meeting_id'});
 499          $meetingidelements = explode('-', $meetingidelements[0]);
 500          if (!isset($bigbluebuttonbn) || $bigbluebuttonbn->meetingid != $meetingidelements[0]) {
 501              return 'HTTP/1.0 410 Gone. The activity may have been deleted';
 502          }
 503  
 504          // We make sure events are processed only once.
 505          $overrides = ['meetingid' => $data->{'meeting_id'}];
 506          $meta['internalmeetingid'] = $data->{'internal_meeting_id'};
 507          $meta['callback'] = 'meeting_events';
 508          $meta['meetingid'] = $data->{'meeting_id'};
 509  
 510          $eventcount = logger::log_event_callback($instance, $overrides, $meta);
 511          if ($eventcount === 1) {
 512              // Process the events.
 513              self::process_meeting_events($instance, $data);
 514              return 'HTTP/1.0 200 Accepted. Enqueued.';
 515          } else {
 516              return 'HTTP/1.0 202 Accepted. Already processed.';
 517          }
 518      }
 519  
 520      /**
 521       * Helper function enqueues list of meeting events to be stored and processed as for completion.
 522       *
 523       * @param instance $instance
 524       * @param stdClass $jsonobj
 525       */
 526      protected static function process_meeting_events(instance $instance, stdClass $jsonobj) {
 527          $meetingid = $jsonobj->{'meeting_id'};
 528          $recordid = $jsonobj->{'internal_meeting_id'};
 529          $attendees = $jsonobj->{'data'}->{'attendees'};
 530          foreach ($attendees as $attendee) {
 531              $userid = $attendee->{'ext_user_id'};
 532              $overrides['meetingid'] = $meetingid;
 533              $overrides['userid'] = $userid;
 534              $meta['recordid'] = $recordid;
 535              $meta['data'] = $attendee;
 536  
 537              // Stores the log.
 538              logger::log_event_summary($instance, $overrides, $meta);
 539  
 540              // Enqueue a task for processing the completion.
 541              bigbluebutton_proxy::enqueue_completion_event($instance->get_instance_data(), $userid);
 542          }
 543      }
 544  
 545      /**
 546       * Prepare join meeting action
 547       *
 548       * @param int $origin
 549       * @return void
 550       */
 551      protected function prepare_meeting_join_action(int $origin) {
 552          $this->do_get_meeting_info(true);
 553          if ($this->is_running()) {
 554              if ($this->instance->has_user_limit_been_reached($this->get_participant_count())) {
 555                  throw new meeting_join_exception('userlimitreached');
 556              }
 557          } else if ($this->instance->user_must_wait_to_join()) {
 558              // If user is not administrator nor moderator (user is student) and waiting is required.
 559              throw new meeting_join_exception('waitformoderator');
 560          }
 561  
 562          // Moodle event logger: Create an event for meeting joined.
 563          logger::log_meeting_joined_event($this->instance, $origin);
 564  
 565          // Before executing the redirect, increment the number of participants.
 566          roles::participant_joined($this->instance->get_meeting_id(), $this->instance->is_moderator());
 567      }
 568      /**
 569       * Join a meeting.
 570       *
 571       * @param int $origin The spec
 572       * @return string The URL to redirect to
 573       * @throws meeting_join_exception
 574       */
 575      public function join(int $origin): string {
 576          $this->prepare_meeting_join_action($origin);
 577          return $this->get_join_url();
 578      }
 579  
 580      /**
 581       * Join a meeting as a guest.
 582       *
 583       * @param int $origin The spec
 584       * @param string $userfullname Fullname for the guest user
 585       * @return string The URL to redirect to
 586       * @throws meeting_join_exception
 587       */
 588      public function guest_join(int $origin, string $userfullname): string {
 589          $this->prepare_meeting_join_action($origin);
 590          return $this->get_join_url();
 591      }
 592  }