Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   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 communication_matrix;
  18  
  19  use communication_matrix\local\spec\features\matrix\{
  20      create_room_v3 as create_room_feature,
  21      get_room_members_v3 as get_room_members_feature,
  22      remove_member_from_room_v3 as remove_member_from_room_feature,
  23      update_room_avatar_v3 as update_room_avatar_feature,
  24      update_room_name_v3 as update_room_name_feature,
  25      update_room_topic_v3 as update_room_topic_feature,
  26      upload_content_v3 as upload_content_feature,
  27      media_create_v1 as media_create_feature,
  28  };
  29  use communication_matrix\local\spec\features\synapse\{
  30      create_user_v2 as create_user_feature,
  31      get_room_info_v1 as get_room_info_feature,
  32      get_user_info_v2 as get_user_info_feature,
  33      invite_member_to_room_v1 as invite_member_to_room_feature,
  34  };
  35  use core_communication\processor;
  36  use stdClass;
  37  use GuzzleHttp\Psr7\Response;
  38  
  39  /**
  40   * class communication_feature to handle matrix specific actions.
  41   *
  42   * @package    communication_matrix
  43   * @copyright  2023 Safat Shahin <safat.shahin@moodle.com>
  44   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  45   */
  46  class communication_feature implements
  47      \core_communication\communication_provider,
  48      \core_communication\form_provider,
  49      \core_communication\room_chat_provider,
  50      \core_communication\room_user_provider,
  51      \core_communication\user_provider {
  52      /** @var ?matrix_room $room The matrix room object to update room information */
  53      private ?matrix_room $room = null;
  54  
  55      /** @var string|null The URI of the home server */
  56      protected ?string $homeserverurl = null;
  57  
  58      /** @var string The URI of the Matrix web client */
  59      protected string $webclienturl;
  60  
  61      /** @var \communication_matrix\local\spec\v1p1|null The Matrix API processor */
  62      protected ?matrix_client $matrixapi;
  63  
  64      /**
  65       * Load the communication provider for the communication api.
  66       *
  67       * @param processor $communication The communication processor object
  68       * @return communication_feature The communication provider object
  69       */
  70      public static function load_for_instance(processor $communication): self {
  71          return new self($communication);
  72      }
  73  
  74      /**
  75       * Reload the room information.
  76       * This may be necessary after a room has been created or updated via the adhoc task.
  77       * This is primarily intended for use in unit testing, but may have real world cases too.
  78       */
  79      public function reload(): void {
  80          $this->room = null;
  81          $this->processor = processor::load_by_id($this->processor->get_id());
  82      }
  83  
  84      /**
  85       * Constructor for communication provider to initialize necessary objects for api cals etc..
  86       *
  87       * @param processor $processor The communication processor object
  88       */
  89      private function __construct(
  90          private \core_communication\processor $processor,
  91      ) {
  92          $this->homeserverurl = get_config('communication_matrix', 'matrixhomeserverurl');
  93          $this->webclienturl = get_config('communication_matrix', 'matrixelementurl');
  94  
  95          if ($processor::is_provider_available('communication_matrix')) {
  96              // Generate the API instance.
  97              $this->matrixapi = matrix_client::instance(
  98                  serverurl: $this->homeserverurl,
  99                  accesstoken: get_config('communication_matrix', 'matrixaccesstoken'),
 100              );
 101          }
 102      }
 103  
 104      /**
 105       * Check whether the room configuration has been created yet.
 106       *
 107       * @return bool
 108       */
 109      protected function room_exists(): bool {
 110          return (bool) $this->get_room_configuration();
 111      }
 112  
 113      /**
 114       * Whether the room exists on the remote server.
 115       * This does not involve a remote call, but checks whether Moodle is aware of the room id.
 116       * @return bool
 117       */
 118      protected function remote_room_exists(): bool {
 119          $room = $this->get_room_configuration();
 120  
 121          return $room && ($room->get_room_id() !== null);
 122      }
 123  
 124      /**
 125       * Get the stored room configuration.
 126       * @return null|matrix_room
 127       */
 128      public function get_room_configuration(): ?matrix_room {
 129          $this->room = matrix_room::load_by_processor_id($this->processor->get_id());
 130          return $this->room;
 131      }
 132  
 133      /**
 134       * Return the current room id.
 135       *
 136       * @return string|null
 137       */
 138      public function get_room_id(): ?string {
 139          return $this->get_room_configuration()?->get_room_id();
 140      }
 141  
 142      /**
 143       * Create members.
 144       *
 145       * @param array $userids The Moodle user ids to create
 146       */
 147      public function create_members(array $userids): void {
 148          $addedmembers = [];
 149  
 150          // This API requiures the create_user feature.
 151          $this->matrixapi->require_feature(create_user_feature::class);
 152  
 153          foreach ($userids as $userid) {
 154              $user = \core_user::get_user($userid);
 155              $userfullname = fullname($user);
 156  
 157              // Proceed if we have a user's full name and email to work with.
 158              if (!empty($user->email) && !empty($userfullname)) {
 159                  $qualifiedmuid = matrix_user_manager::get_formatted_matrix_userid($user->username);
 160  
 161                  // First create user in matrix.
 162                  $response = $this->matrixapi->create_user(
 163                      userid: $qualifiedmuid,
 164                      displayname: $userfullname,
 165                      threepids: [(object) [
 166                          'medium' => 'email',
 167                          'address' => $user->email,
 168                      ], ],
 169                      externalids: [],
 170                  );
 171                  $body = json_decode($response->getBody());
 172  
 173                  if (!empty($matrixuserid = $body->name)) {
 174                      // Then create matrix user id in moodle.
 175                      matrix_user_manager::set_matrix_userid_in_moodle($userid, $qualifiedmuid);
 176                      if ($this->add_registered_matrix_user_to_room($matrixuserid)) {
 177                          $addedmembers[] = $userid;
 178                      }
 179                  }
 180              }
 181          }
 182  
 183          // Set the power level of the users.
 184          if (!empty($addedmembers) && $this->is_power_levels_update_required($addedmembers)) {
 185              $this->set_matrix_power_levels();
 186          }
 187  
 188          // Mark then users as synced for the added members.
 189          $this->processor->mark_users_as_synced($addedmembers);
 190      }
 191  
 192      public function update_room_membership(array $userids): void {
 193          $this->set_matrix_power_levels();
 194          // Mark the users as synced for the updated members.
 195          $this->processor->mark_users_as_synced($userids);
 196      }
 197  
 198      /**
 199       * Add members to a room.
 200       *
 201       * @param array $userids The user ids to add
 202       */
 203      public function add_members_to_room(array $userids): void {
 204          $unregisteredmembers = [];
 205          $addedmembers = [];
 206  
 207          foreach ($userids as $userid) {
 208              $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($userid);
 209  
 210              if ($matrixuserid && $this->check_user_exists($matrixuserid)) {
 211                  if ($this->add_registered_matrix_user_to_room($matrixuserid)) {
 212                      $addedmembers[] = $userid;
 213                  }
 214              } else {
 215                  $unregisteredmembers[] = $userid;
 216              }
 217          }
 218  
 219          // Set the power level of the users.
 220          if (!empty($addedmembers) && $this->is_power_levels_update_required($addedmembers)) {
 221              $this->set_matrix_power_levels();
 222          }
 223  
 224          // Mark then users as synced for the added members.
 225          $this->processor->mark_users_as_synced($addedmembers);
 226  
 227          // Create Matrix users.
 228          if (count($unregisteredmembers) > 0) {
 229              $this->create_members($unregisteredmembers);
 230          }
 231      }
 232  
 233      /**
 234       * Adds the registered matrix user id to room.
 235       *
 236       * @param string $matrixuserid Registered matrix user id
 237       */
 238      private function add_registered_matrix_user_to_room(string $matrixuserid): bool {
 239          // Require the invite_member_to_room API feature.
 240          $this->matrixapi->require_feature(invite_member_to_room_feature::class);
 241  
 242          if (!$this->check_room_membership($matrixuserid)) {
 243              $response = $this->matrixapi->invite_member_to_room(
 244                  roomid: $this->get_room_id(),
 245                  userid: $matrixuserid,
 246              );
 247  
 248              $body = self::get_body($response);
 249              if (empty($body->room_id)) {
 250                  return false;
 251              }
 252  
 253              if ($body->room_id !== $this->get_room_id()) {
 254                  return false;
 255              }
 256  
 257              return true;
 258          }
 259          return false;
 260      }
 261  
 262      /**
 263       * Remove members from a room.
 264       *
 265       * @param array $userids The Moodle user ids to remove
 266       */
 267      public function remove_members_from_room(array $userids): void {
 268          // This API requiures the remove_members_from_room feature.
 269          $this->matrixapi->require_feature(remove_member_from_room_feature::class);
 270  
 271          if ($this->get_room_id() === null) {
 272              return;
 273          }
 274  
 275          // Remove the power level for the user first.
 276          $this->set_matrix_power_levels($userids);
 277  
 278          $membersremoved = [];
 279  
 280          $currentpowerlevels = $this->get_current_powerlevel_data();
 281          $currentuserpowerlevels = (array) $currentpowerlevels->users ?? [];
 282  
 283          foreach ($userids as $userid) {
 284              // Check user is member of room first.
 285              $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($userid);
 286  
 287              if (!$matrixuserid) {
 288                  // Unable to find a matrix userid for this user.
 289                  continue;
 290              }
 291  
 292              if (array_key_exists($matrixuserid, $currentuserpowerlevels)) {
 293                  if ($currentuserpowerlevels[$matrixuserid] >= matrix_constants::POWER_LEVEL_MAXIMUM) {
 294                      // Skip removing the user if they are an admin.
 295                      continue;
 296                  }
 297              }
 298  
 299              if (
 300                  $this->check_user_exists($matrixuserid) &&
 301                  $this->check_room_membership($matrixuserid)
 302              ) {
 303                  $this->matrixapi->remove_member_from_room(
 304                      roomid: $this->get_room_id(),
 305                      userid: $matrixuserid,
 306                  );
 307  
 308                  $membersremoved[] = $userid;
 309              }
 310          }
 311  
 312          $this->processor->delete_instance_user_mapping($membersremoved);
 313      }
 314  
 315      /**
 316       * Check if a user exists in Matrix.
 317       * Use if user existence is needed before doing something else.
 318       *
 319       * @param string $matrixuserid The Matrix user id to check
 320       * @return bool
 321       */
 322      public function check_user_exists(string $matrixuserid): bool {
 323          // This API requires the get_user_info feature.
 324          $this->matrixapi->require_feature(get_user_info_feature::class);
 325  
 326          $response = $this->matrixapi->get_user_info(
 327              userid: $matrixuserid,
 328          );
 329          $body = self::get_body($response);
 330  
 331          return isset($body->name);
 332      }
 333  
 334      /**
 335       * Check if a user is a member of a room.
 336       * Use if membership confirmation is needed before doing something else.
 337       *
 338       * @param string $matrixuserid The Matrix user id to check
 339       * @return bool
 340       */
 341      public function check_room_membership(string $matrixuserid): bool {
 342          // This API requires the get_room_members feature.
 343          $this->matrixapi->require_feature(get_room_members_feature::class);
 344  
 345          $response = $this->matrixapi->get_room_members(
 346              roomid: $this->get_room_id(),
 347          );
 348          $body = self::get_body($response);
 349  
 350          // Check user id is in the returned room member ids.
 351          return isset($body->joined) && array_key_exists($matrixuserid, (array) $body->joined);
 352      }
 353  
 354      /**
 355       * Create a room based on the data in the communication instance.
 356       *
 357       * @return bool
 358       */
 359      public function create_chat_room(): bool {
 360          if ($this->remote_room_exists()) {
 361              // A room already exists. Update it instead.
 362              return $this->update_chat_room();
 363          }
 364  
 365          // This method requires the create_room API feature.
 366          $this->matrixapi->require_feature(create_room_feature::class);
 367  
 368          $room = $this->get_room_configuration();
 369  
 370          $response = $this->matrixapi->create_room(
 371              name: $this->processor->get_room_name(),
 372              visibility: 'private',
 373              preset: 'private_chat',
 374              initialstate: [],
 375              options: [
 376                  'topic' => $room->get_topic(),
 377              ],
 378          );
 379  
 380          $response = self::get_body($response);
 381  
 382          if (empty($response->room_id)) {
 383              throw new \moodle_exception(
 384                  'Unable to determine ID of matrix room',
 385              );
 386          }
 387  
 388          // Update our record of the matrix room_id.
 389          $room->update_room_record(
 390              roomid: $response->room_id,
 391          );
 392  
 393          // Update the room avatar.
 394          $this->update_room_avatar();
 395          return true;
 396      }
 397  
 398      public function update_chat_room(): bool {
 399          if (!$this->remote_room_exists()) {
 400              // No room exists. Create it instead.
 401              return $this->create_chat_room();
 402          }
 403  
 404          $this->matrixapi->require_features([
 405              get_room_info_feature::class,
 406              update_room_name_feature::class,
 407              update_room_topic_feature::class,
 408          ]);
 409  
 410          // Get room data.
 411          $response = $this->matrixapi->get_room_info(
 412              roomid: $this->get_room_id(),
 413          );
 414          $remoteroomdata = self::get_body($response);
 415  
 416          // Update the room name when it's updated from the form.
 417          if ($remoteroomdata->name !== $this->processor->get_room_name()) {
 418              $this->matrixapi->update_room_name(
 419                  roomid: $this->get_room_id(),
 420                  name: $this->processor->get_room_name(),
 421              );
 422          }
 423  
 424          // Update the room topic if set.
 425          $localroomdata = $this->get_room_configuration();
 426          if ($remoteroomdata->topic !== $localroomdata->get_topic()) {
 427              $this->matrixapi->update_room_topic(
 428                  roomid: $localroomdata->get_room_id(),
 429                  topic: $localroomdata->get_topic(),
 430              );
 431          }
 432  
 433          // Update room avatar.
 434          $this->update_room_avatar();
 435  
 436          return true;
 437      }
 438  
 439      public function delete_chat_room(): bool {
 440          $this->get_room_configuration()->delete_room_record();
 441          $this->room = null;
 442  
 443          return true;
 444      }
 445  
 446      /**
 447       * Update the room avatar when an instance image is added or updated.
 448       */
 449      public function update_room_avatar(): void {
 450          // Both of the following features of the remote API are required.
 451          $this->matrixapi->require_features([
 452              upload_content_feature::class,
 453              update_room_avatar_feature::class,
 454          ]);
 455  
 456          // Check if we have an avatar that needs to be synced.
 457          if ($this->processor->is_avatar_synced()) {
 458              return;
 459          }
 460  
 461          $instanceimage = $this->processor->get_avatar();
 462          $contenturi = null;
 463  
 464          if ($this->matrixapi->implements_feature(media_create_feature::class)) {
 465              // From version 1.7 we can fetch a mxc URI and use it before uploading the content.
 466              if ($instanceimage) {
 467                  $response = $this->matrixapi->media_create();
 468                  $contenturi = self::get_body($response)->content_uri;
 469  
 470                  // Now update the room avatar.
 471                  $response = $this->matrixapi->update_room_avatar(
 472                      roomid: $this->get_room_id(),
 473                      avatarurl: $contenturi,
 474                  );
 475  
 476                  // And finally upload the content.
 477                  $this->matrixapi->upload_content($instanceimage);
 478              } else {
 479                  $response = $this->matrixapi->update_room_avatar(
 480                      roomid: $this->get_room_id(),
 481                      avatarurl: null,
 482                  );
 483              }
 484          } else {
 485              // Prior to v1.7 the only way to upload content was to upload the content, which returns a mxc URI to use.
 486  
 487              if ($instanceimage) {
 488                  // First upload the content.
 489                  $response = $this->matrixapi->upload_content($instanceimage);
 490                  $body = self::get_body($response);
 491                  $contenturi = $body->content_uri;
 492              }
 493  
 494              // Now update the room avatar.
 495              $response = $this->matrixapi->update_room_avatar(
 496                  roomid: $this->get_room_id(),
 497                  avatarurl: $contenturi,
 498              );
 499          }
 500  
 501          // Indicate the avatar has been synced if it was successfully set with Matrix.
 502          if ($response->getReasonPhrase() === 'OK') {
 503              $this->processor->set_avatar_synced_flag(true);
 504          }
 505      }
 506  
 507      public function get_chat_room_url(): ?string {
 508          if (!$this->get_room_id()) {
 509              // We don't have a room id for this record.
 510              return null;
 511          }
 512  
 513          return sprintf(
 514              "%s#/room/%s",
 515              $this->webclienturl,
 516              $this->get_room_id(),
 517          );
 518      }
 519  
 520      public function save_form_data(\stdClass $instance): void {
 521          $matrixroomtopic = $instance->matrixroomtopic ?? null;
 522          $room = $this->get_room_configuration();
 523  
 524          if ($room) {
 525              $room->update_room_record(
 526                  topic: $matrixroomtopic,
 527              );
 528          } else {
 529              $this->room = matrix_room::create_room_record(
 530                  processorid: $this->processor->get_id(),
 531                  topic: $matrixroomtopic,
 532              );
 533          }
 534      }
 535  
 536      public function set_form_data(\stdClass $instance): void {
 537          if (!empty($instance->id) && !empty($this->processor->get_id())) {
 538              if ($this->room_exists()) {
 539                  $instance->matrixroomtopic = $this->get_room_configuration()->get_topic();
 540              }
 541          }
 542      }
 543  
 544      public static function set_form_definition(\MoodleQuickForm $mform): void {
 545          // Room description for the communication provider.
 546          $mform->insertElementBefore($mform->createElement(
 547              'text',
 548              'matrixroomtopic',
 549              get_string('matrixroomtopic', 'communication_matrix'),
 550              'maxlength="255" size="20"'
 551          ), 'addcommunicationoptionshere');
 552          $mform->addHelpButton('matrixroomtopic', 'matrixroomtopic', 'communication_matrix');
 553          $mform->setType('matrixroomtopic', PARAM_TEXT);
 554      }
 555  
 556      /**
 557       * Get the body of a response as a stdClass.
 558       *
 559       * @param Response $response
 560       * @return stdClass
 561       */
 562      public static function get_body(Response $response): stdClass {
 563          $body = $response->getBody();
 564  
 565          return json_decode($body, false, 512, JSON_THROW_ON_ERROR);
 566      }
 567  
 568      /**
 569       * Set the matrix power level with the room.
 570       *
 571       * Users with a non-moodle power level are not typically removed unless specified in the $forceremoval param.
 572       * Matrix Admin users are never removed.
 573       *
 574       * @param array $forceremoval The users to force removal from the room, even if they have a custom power level
 575       */
 576      private function set_matrix_power_levels(
 577          array $forceremoval = [],
 578      ): void {
 579          // Get the current power levels.
 580          $currentpowerlevels = $this->get_current_powerlevel_data();
 581          $currentuserpowerlevels = (array) $currentpowerlevels->users ?? [];
 582  
 583          // Get all the current users who need to be in the room.
 584          $userlist = $this->processor->get_all_userids_for_instance();
 585  
 586          // Translate the user ids to matrix user ids.
 587          $userlist = array_combine(
 588              array_map(
 589                  fn ($userid) => matrix_user_manager::get_matrixid_from_moodle($userid),
 590                  $userlist,
 591              ),
 592              $userlist,
 593          );
 594  
 595          // Determine the power levels, and filter out anyone with the default level.
 596          $newuserpowerlevels = array_filter(
 597              array_map(
 598                  fn($userid) => $this->get_user_allowed_power_level($userid),
 599                  $userlist,
 600              ),
 601              fn($level) => $level !== matrix_constants::POWER_LEVEL_DEFAULT,
 602          );
 603  
 604          // Keep current room admins, and users which don't use our MODERATOR power level without changing them.
 605          $staticusers = $this->get_users_with_custom_power_level($currentuserpowerlevels);
 606          foreach ($staticusers as $userid => $level) {
 607              $newuserpowerlevels[$userid] = $level;
 608          }
 609  
 610          if (!empty($forceremoval)) {
 611              // Remove the users from the power levels if they are not admins.
 612              foreach ($forceremoval as $userid) {
 613                  if ($newuserpowerlevels < matrix_constants::POWER_LEVEL_MAXIMUM) {
 614                      unset($newuserpowerlevels[$userid]);
 615                  }
 616              }
 617          }
 618  
 619          if (!$this->power_levels_changed($currentuserpowerlevels, $newuserpowerlevels)) {
 620              // No changes to make.
 621              return;
 622          }
 623  
 624  
 625          // Update the power levels for the room.
 626          $this->matrixapi->update_room_power_levels(
 627              roomid: $this->get_room_id(),
 628              users: $newuserpowerlevels,
 629          );
 630      }
 631  
 632      /**
 633       * Filter the list of users provided to remove those with a moodle-related power level.
 634       *
 635       * @param array $users
 636       * @return array
 637       */
 638      private function get_users_with_custom_power_level(array $users): array {
 639          return array_filter(
 640              $users,
 641              function ($level): bool {
 642                  switch ($level) {
 643                      case matrix_constants::POWER_LEVEL_DEFAULT:
 644                      case matrix_constants::POWER_LEVEL_MOODLE_SITE_ADMIN:
 645                      case matrix_constants::POWER_LEVEL_MOODLE_MODERATOR:
 646                          return false;
 647                      default:
 648                          return true;
 649                  }
 650              },
 651          );
 652      }
 653  
 654      /**
 655       * Check whether power levels have changed compared with the proposed power levels.
 656       *
 657       * @param array $currentuserpowerlevels The current power levels
 658       * @param array $newuserpowerlevels The new power levels proposed
 659       * @return bool Whether there is any change to be made
 660       */
 661      private function power_levels_changed(
 662          array $currentuserpowerlevels,
 663          array $newuserpowerlevels,
 664      ): bool {
 665          if (count($newuserpowerlevels) !== count($currentuserpowerlevels)) {
 666              // Different number of keys - there must be a difference then.
 667              return true;
 668          }
 669  
 670          // Sort the power levels.
 671          ksort($newuserpowerlevels, SORT_NUMERIC);
 672  
 673          // Get the current power levels.
 674          ksort($currentuserpowerlevels);
 675  
 676          $diff = array_merge(
 677              array_diff_assoc(
 678                  $newuserpowerlevels,
 679                  $currentuserpowerlevels,
 680              ),
 681              array_diff_assoc(
 682                  $currentuserpowerlevels,
 683                  $newuserpowerlevels,
 684              ),
 685          );
 686  
 687          return count($diff) > 0;
 688      }
 689  
 690      /**
 691       * Get the current power level for the room.
 692       *
 693       * @return stdClass
 694       */
 695      private function get_current_powerlevel_data(): \stdClass {
 696          $roomid = $this->get_room_id();
 697          $response = $this->matrixapi->get_room_power_levels(
 698              roomid: $roomid,
 699          );
 700          if ($response->getStatusCode() !== 200) {
 701              throw new \moodle_exception(
 702                  'Unable to get power levels for room',
 703              );
 704          }
 705  
 706          $powerdata = $this->get_body($response);
 707          $powerdata = array_filter(
 708              $powerdata->rooms->join->{$roomid}->state->events,
 709              fn($value) => $value->type === 'm.room.power_levels'
 710          );
 711          $powerdata = reset($powerdata);
 712  
 713          return $powerdata->content;
 714      }
 715  
 716      /**
 717       * Determine if a power level update is required.
 718       * Matrix will always set a user to the default power level of 0 when a power level update is made.
 719       * That is, unless we specify another level. As long as one person's level is greater than the default,
 720       * we will need to set the power levels of all users greater than the default.
 721       *
 722       * @param array $userids The users to evaluate
 723       * @return boolean Returns true if an update is required
 724       */
 725      private function is_power_levels_update_required(array $userids): bool {
 726          // Is the user's power level greater than the default?
 727          foreach ($userids as $userid) {
 728              if ($this->get_user_allowed_power_level($userid) > matrix_constants::POWER_LEVEL_DEFAULT) {
 729                  return true;
 730              }
 731          }
 732          return false;
 733      }
 734  
 735      /**
 736       * Get the allowed power level for the user id according to perms/site admin or default.
 737       *
 738       * @param int $userid
 739       * @return int
 740       */
 741      public function get_user_allowed_power_level(int $userid): int {
 742          $powerlevel = matrix_constants::POWER_LEVEL_DEFAULT;
 743  
 744          if (has_capability('communication/matrix:moderator', $this->processor->get_context(), $userid)) {
 745              $powerlevel = matrix_constants::POWER_LEVEL_MOODLE_MODERATOR;
 746          }
 747  
 748          // If site admin, override all caps.
 749          if (is_siteadmin($userid)) {
 750              $powerlevel = matrix_constants::POWER_LEVEL_MOODLE_SITE_ADMIN;
 751          }
 752  
 753          return $powerlevel;
 754      }
 755  
 756      /*
 757       * Check if matrix settings are configured
 758       *
 759       * @return boolean
 760       */
 761      public static function is_configured(): bool {
 762          // Matrix communication settings.
 763          $matrixhomeserverurl = get_config('communication_matrix', 'matrixhomeserverurl');
 764          $matrixaccesstoken = get_config('communication_matrix', 'matrixaccesstoken');
 765          $matrixelementurl = get_config('communication_matrix', 'matrixelementurl');
 766  
 767          if (
 768              !empty($matrixhomeserverurl) &&
 769              !empty($matrixaccesstoken) &&
 770              (PHPUNIT_TEST || defined('BEHAT_SITE_RUNNING') || !empty($matrixelementurl))
 771          ) {
 772              return true;
 773          }
 774          return false;
 775      }
 776  }