From 914686bc5efd60dcdbc045bdbd235ae4118a028f Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 13 Jun 2023 15:12:16 +0800 Subject: [PATCH] MDL-77917 communication_matrix: Support server API versions This commit brings in support for multiple versions of the Matrix specification. A Matrix server is compromised of a number of individually versioned API endpoints, for example: /_matrix/client/v3/createRoom /_matrix/client/v3/rooms/:roomid/joined_members /_matrix/media/v1/create The combination of a large number of these individually versioned endpoints forms a Matrix Specification version. For example: * the /_matrix/media/v1/create endpoint was created for version 1.7 of the specification, and does not exist in earlier versions. * in the future a new behaviour or parameter may be created for the `createRoom` endpoint and a new endpoint created at: /_matrix/client/v4/createRoom A single server can support multiple versions of the Matrix specification. The server declares the versions of the specification that it supports using a non-versioned endpoint at `/_matrix/client/versions`. As a Matrix client, Moodle should: * query the server version endpoint * determine the combination of mutually supported Matrix specification versions * create a client instance of the highest-supported version of the specification. For example, if Moodle (Matrix client) and a remote server have the following support: ``` Moodle: 1.1 1.2 1.3 1.4 1.5 1.6 1.7 Server: r0 1.1 1.2 1.3 1.4 1.5 1.6 ``` The versions in common are 1.1 through 1.6, and version 1.6 would be chosen. To avoid duplication and allow for support of future features more easily, the Moodle client is written as: * a set of classes named `v1p1` through `v1p7` (currently) which extend the `matrix_client` abstract class; and * a set if PHP traits which provide the implementation for individual versioned endpoints. Each client version then imports any relevant traits which are present in that version of the Matrix Specification. For example versions 1.1 to 1.6 do _not_ have the `/_matrix/media/v1/create` endpoint so they do not import this trait. This trait was introduced in version 1.7, so the trait is included from that version onwards. In the future, if an endpoint is created which conflicts with an existing endpoint, then it would be easy to create a new client version which uses the existing common traits, and adds the new trait. Each endpoint is written using a `command` class which extends the Guzzle implementation of the PSR-7 Request interface. This command class adds support for easy creation of: * path parameters within the URI * query parameters * body parameters This is done to avoid complex patterns of Request creation which are repeated for every client endpoint. --- communication/classes/api.php | 123 ++- communication/classes/processor.php | 44 +- .../matrix/classes/communication_feature.php | 360 +++++-- .../provider/matrix/classes/local/command.php | 189 ++++ .../spec/features/matrix/create_room_v3.php | 79 ++ .../features/matrix/get_room_members_v3.php | 53 + .../matrix/remove_member_from_room_v3.php | 58 ++ .../features/matrix/update_room_avatar_v3.php | 60 ++ .../features/matrix/update_room_name_v3.php | 55 + .../features/matrix/update_room_topic_v3.php | 56 + .../features/matrix/upload_content_v3.php | 66 ++ .../spec/features/synapse/create_user_v2.php | 67 ++ .../features/synapse/get_room_info_v1.php | 51 + .../features/synapse/get_user_info_v2.php | 53 + .../synapse/invite_member_to_room_v1.php | 57 ++ .../matrix/classes/local/spec/v1p1.php | 45 + .../matrix/classes/local/spec/v1p2.php | 30 + .../matrix/classes/local/spec/v1p3.php | 30 + .../matrix/classes/local/spec/v1p4.php | 30 + .../matrix/classes/local/spec/v1p5.php | 30 + .../matrix/classes/local/spec/v1p6.php | 30 + .../matrix/classes/local/spec/v1p7.php | 33 + .../provider/matrix/classes/matrix_client.php | 341 +++++++ .../matrix/classes/matrix_events_manager.php | 261 ----- .../provider/matrix/classes/matrix_rooms.php | 134 +-- .../matrix/classes/privacy/provider.php | 1 + communication/provider/matrix/db/caches.php | 42 + .../matrix/lang/en/communication_matrix.php | 1 + .../tests/communication_feature_test.php | 409 ++++---- .../tests/fixtures/mocked_matrix_client.php | 52 + .../matrix/tests/local/command_test.php | 412 ++++++++ .../matrix/tests/matrix_client_test.php | 413 ++++++++ .../matrix/tests/matrix_client_test_trait.php | 169 ++++ .../tests/matrix_communication_test.php | 956 ------------------ .../tests/matrix_events_manager_test.php | 105 -- .../matrix/tests/matrix_rooms_test.php | 205 ++-- .../matrix/tests/matrix_test_helper_trait.php | 51 +- .../matrix/tests/matrix_user_manager_test.php | 1 - communication/tests/api_test.php | 65 +- course/lib.php | 10 +- 40 files changed, 3326 insertions(+), 1901 deletions(-) create mode 100644 communication/provider/matrix/classes/local/command.php create mode 100644 communication/provider/matrix/classes/local/spec/features/matrix/create_room_v3.php create mode 100644 communication/provider/matrix/classes/local/spec/features/matrix/get_room_members_v3.php create mode 100644 communication/provider/matrix/classes/local/spec/features/matrix/remove_member_from_room_v3.php create mode 100644 communication/provider/matrix/classes/local/spec/features/matrix/update_room_avatar_v3.php create mode 100644 communication/provider/matrix/classes/local/spec/features/matrix/update_room_name_v3.php create mode 100644 communication/provider/matrix/classes/local/spec/features/matrix/update_room_topic_v3.php create mode 100644 communication/provider/matrix/classes/local/spec/features/matrix/upload_content_v3.php create mode 100644 communication/provider/matrix/classes/local/spec/features/synapse/create_user_v2.php create mode 100644 communication/provider/matrix/classes/local/spec/features/synapse/get_room_info_v1.php create mode 100644 communication/provider/matrix/classes/local/spec/features/synapse/get_user_info_v2.php create mode 100644 communication/provider/matrix/classes/local/spec/features/synapse/invite_member_to_room_v1.php create mode 100644 communication/provider/matrix/classes/local/spec/v1p1.php create mode 100644 communication/provider/matrix/classes/local/spec/v1p2.php create mode 100644 communication/provider/matrix/classes/local/spec/v1p3.php create mode 100644 communication/provider/matrix/classes/local/spec/v1p4.php create mode 100644 communication/provider/matrix/classes/local/spec/v1p5.php create mode 100644 communication/provider/matrix/classes/local/spec/v1p6.php create mode 100644 communication/provider/matrix/classes/local/spec/v1p7.php create mode 100644 communication/provider/matrix/classes/matrix_client.php delete mode 100644 communication/provider/matrix/classes/matrix_events_manager.php create mode 100644 communication/provider/matrix/db/caches.php create mode 100644 communication/provider/matrix/tests/fixtures/mocked_matrix_client.php create mode 100644 communication/provider/matrix/tests/local/command_test.php create mode 100644 communication/provider/matrix/tests/matrix_client_test.php create mode 100644 communication/provider/matrix/tests/matrix_client_test_trait.php delete mode 100644 communication/provider/matrix/tests/matrix_communication_test.php delete mode 100644 communication/provider/matrix/tests/matrix_events_manager_test.php diff --git a/communication/classes/api.php b/communication/classes/api.php index 2cc4dd7bdf0..b9840cd1992 100644 --- a/communication/classes/api.php +++ b/communication/classes/api.php @@ -84,6 +84,62 @@ class api { return new self($component, $instancetype, $instanceid); } + /** + * Reload in the internal instance data. + */ + public function reload(): void { + $this->communication = processor::load_by_instance( + $this->component, + $this->instancetype, + $this->instanceid, + ); + } + + /** + * Return the underlying communication processor object. + * + * @return processor + */ + public function get_processor(): processor { + return $this->communication; + } + + /** + * Return the room provider. + * + * @return \core_communication\room_chat_provider + */ + public function get_room_provider(): \core_communication\room_chat_provider { + return $this->communication->get_room_provider(); + } + + /** + * Return the user provider. + * + * @return \core_communication\user_provider + */ + public function get_user_provider(): \core_communication\user_provider { + return $this->communication->get_user_provider(); + } + + /** + * Return the room user provider. + * + * @return \core_communication\room_user_provider + */ + public function get_room_user_provider(): \core_communication\room_user_provider { + return $this->communication->get_room_user_provider(); + } + + /** + * Return the form provider. + * + * @return \core_communication\form_provider + */ + public function get_form_provider(): \core_communication\form_provider { + return $this->communication->get_form_provider(); + } + /** * Check if the communication api is enabled. */ @@ -196,25 +252,39 @@ class api { } + /** + * Get the avatar file. + * + * @return null|\stored_file + */ + public function get_avatar(): ?\stored_file { + $filename = $this->communication->get_avatar_filename(); + if ($filename === null) { + return null; + } + $fs = get_file_storage(); + $args = (array) $this->get_avatar_filerecord($filename); + return $fs->get_file(...$args) ?: null; + } + /** * Get the avatar file record for the avatar for filesystem. * * @param string $filename The filename of the avatar * @return stdClass */ - public function get_avatar_filerecord(string $filename): stdClass { + protected function get_avatar_filerecord(string $filename): stdClass { return (object) [ - 'contextid' => \context_system::instance()->id, + 'contextid' => \core\context\system::instance()->id, 'component' => 'core_communication', 'filearea' => 'avatar', - 'filename' => $filename, - 'filepath' => '/', 'itemid' => $this->communication->get_id(), + 'filepath' => '/', + 'filename' => $filename, ]; } /** - * * Get the avatar file. * * If null is set, then delete the old area file and set the avatarfilename to null. @@ -229,8 +299,8 @@ class api { return false; } - $currentfilerecord = $this->communication->get_avatar(); - if ($avatar && !empty($currentfilerecord)) { + $currentfilerecord = $this->get_avatar(); + if ($avatar && $currentfilerecord) { $currentfilehash = $currentfilerecord->get_contenthash(); $updatedfilehash = $avatar->get_contenthash(); @@ -240,7 +310,7 @@ class api { } } - $context = \context_system::instance(); + $context = \core\context\system::instance(); $fs = get_file_storage(); $fs->delete_area_files( @@ -266,6 +336,15 @@ class api { return true; } + /** + * A helper to fetch the room name + * + * @return string + */ + public function get_room_name(): string { + return $this->communication->get_room_name(); + } + /** * Set the form data if the data is already available. * @@ -344,20 +423,33 @@ class api { * @param \stdClass|null $instance The actual instance object */ public function update_room( - string $selectedprovider, - string $communicationroomname, + ?string $selectedprovider = null, + ?string $communicationroomname = null, ?\stored_file $avatar = null, ?\stdClass $instance = null, ): void { - // Existing object found, let's update the communication record and associated actions. if ($this->communication !== null) { // Get the previous data to compare for update. - $previousroomname = $this->communication->get_room_name(); $previousprovider = $this->communication->get_provider(); + if ($previousprovider === $selectedprovider) { + // If the provider is the same, unset it. + $selectedprovider = null; + } - // Update communication record. - $this->communication->update_instance($selectedprovider, $communicationroomname); + $previousroomname = $this->communication->get_room_name(); + if ($previousroomname === $communicationroomname) { + // If the room name is the same, we don't need to update the room. + $communicationroomname = null; + } + + if ($selectedprovider !== null || $communicationroomname !== null) { + // Something to update. Update communication record. + $this->communication->update_instance( + provider: $selectedprovider, + roomname: $communicationroomname, + ); + } // Update provider record from form data. if ($instance !== null) { @@ -365,7 +457,8 @@ class api { } // Update the avatar. - $imageupdaterequired = $this->set_avatar($avatar); + // If the value is `null`, then unset the avatar. + $this->set_avatar($avatar); // If the provider is none, we don't need to do anything from room point of view. if ($this->communication->get_provider() === processor::PROVIDER_NONE) { diff --git a/communication/classes/processor.php b/communication/classes/processor.php index 839926c36f4..82f40965302 100644 --- a/communication/classes/processor.php +++ b/communication/classes/processor.php @@ -104,24 +104,29 @@ class processor { } /** - * Update communication instance. + * Update the communication instance with any changes. * - * @param string $provider The communication provider - * @param string $roomname The room name + * @param null|string $provider The communication provider + * @param null|string $roomname The room name */ public function update_instance( - string $provider, - string $roomname, + ?string $provider = null, + ?string $roomname = null, ): void { - global $DB; - if ($provider === self::PROVIDER_NONE) { - $this->instancedata->active = self::PROVIDER_INACTIVE; - } else { - $this->instancedata->provider = $provider; - $this->instancedata->active = self::PROVIDER_ACTIVE; + + if ($provider !== null) { + if ($provider === self::PROVIDER_NONE) { + $this->instancedata->active = self::PROVIDER_INACTIVE; + } else { + $this->instancedata->provider = $provider; + $this->instancedata->active = self::PROVIDER_ACTIVE; + } + } + + if ($roomname !== null) { + $this->instancedata->roomname = $roomname; } - $this->instancedata->roomname = $roomname; $DB->update_record('communication', $this->instancedata); } @@ -470,7 +475,7 @@ class processor { /** * Get communication instance for form feature. * - * @return bool + * @return form_provider */ public function get_form_provider(): form_provider { $this->requires_form_features(); @@ -584,7 +589,7 @@ class processor { $this->instancedata->avatarfilename, ); - return $file ? $file : null; + return $file ?: null; } @@ -595,7 +600,9 @@ class processor { */ public function set_avatar_filename(?string $filename): void { global $DB; - $DB->update_record('communication', ['id' => $this->instancedata->id, 'avatarfilename' => $filename]); + + $this->instancedata->avatarfilename = $filename; + $DB->set_field('communication', 'avatarfilename', $filename, ['id' => $this->instancedata->id]); } /** @@ -613,7 +620,7 @@ class processor { * @return bool */ public function is_avatar_synced(): bool { - return (bool)$this->instancedata->avatarsynced; + return (bool) $this->instancedata->avatarsynced; } /** @@ -623,8 +630,9 @@ class processor { */ public function set_avatar_synced_flag(bool $synced): void { global $DB; - $DB->update_record('communication', ['id' => $this->instancedata->id, 'avatarsynced' => (int)$synced]); - $this->instancedata->avatarsynced = (int)$synced; + + $this->instancedata->avatarsynced = (int) $synced; + $DB->set_field('communication', 'avatarsynced', (int) $synced, ['id' => $this->instancedata->id]); } /** diff --git a/communication/provider/matrix/classes/communication_feature.php b/communication/provider/matrix/classes/communication_feature.php index c5726450899..e955aa1f5d1 100644 --- a/communication/provider/matrix/classes/communication_feature.php +++ b/communication/provider/matrix/classes/communication_feature.php @@ -16,7 +16,24 @@ namespace communication_matrix; +use communication_matrix\local\spec\features\matrix\{ + create_room_v3 as create_room_feature, + get_room_members_v3 as get_room_members_feature, + remove_member_from_room_v3 as remove_member_from_room_feature, + update_room_avatar_v3 as update_room_avatar_feature, + update_room_name_v3 as update_room_name_feature, + update_room_topic_v3 as update_room_topic_feature, + upload_content_v3 as upload_content_feature, +}; +use communication_matrix\local\spec\features\synapse\{ + create_user_v2 as create_user_feature, + get_room_info_v1 as get_room_info_feature, + get_user_info_v2 as get_user_info_feature, + invite_member_to_room_v1 as invite_member_to_room_feature, +}; use core_communication\processor; +use stdClass; +use GuzzleHttp\Psr7\Response; /** * class communication_feature to handle matrix specific actions. @@ -32,11 +49,17 @@ class communication_feature implements \core_communication\room_user_provider, \core_communication\form_provider { - /** @var matrix_events_manager $eventmanager The event manager object to get the endpoints */ - private matrix_events_manager $eventmanager; + /** @var matrix_rooms $room The matrix room object to update room information */ + private ?matrix_rooms $room = null; - /** @var matrix_rooms $matrixrooms The matrix room object to update room information */ - private matrix_rooms $matrixrooms; + /** @var string|null The URI of the home server */ + protected ?string $homeserverurl = null; + + /** @var string The URI of the Matrix web client */ + protected string $webclienturl; + + /** @var \communication_matrix\local\spec\v1p1|null The Matrix API processor */ + protected ?matrix_client $matrixapi; /** * Load the communication provider for the communication api. @@ -48,16 +71,75 @@ class communication_feature implements return new self($communication); } + /** + * Reload the room information. + * This may be necessary after a room has been created or updated via the adhoc task. + * This is primarily intended for use in unit testing, but may have real world cases too. + */ + public function reload(): void { + $this->room = null; + $this->processor = processor::load_by_id($this->processor->get_id()); + } + /** * Constructor for communication provider to initialize necessary objects for api cals etc.. * - * @param processor $communication The communication processor object + * @param processor $processor The communication processor object */ private function __construct( - private \core_communication\processor $communication, + private \core_communication\processor $processor, ) { - $this->matrixrooms = new matrix_rooms($communication->get_id()); - $this->eventmanager = new matrix_events_manager($this->matrixrooms->get_matrix_room_id()); + $this->homeserverurl = get_config('communication_matrix', 'matrixhomeserverurl'); + $this->webclienturl = get_config('communication_matrix', 'matrixelementurl'); + + if ($this->homeserverurl) { + // Generate the API instance. + $this->matrixapi = matrix_client::instance( + serverurl: $this->homeserverurl, + accesstoken: get_config('communication_matrix', 'matrixaccesstoken'), + ); + } + } + + /** + * Check whether the room configuration has been created yet. + * + * @return bool + */ + protected function room_exists(): bool { + return (bool) $this->get_room_configuration(); + } + + /** + * Whether the room exists on the remote server. + * This does not involve a remote call, but checks whether Moodle is aware of the room id. + * @return bool + */ + protected function remote_room_exists(): bool { + $room = $this->get_room_configuration(); + + return $room && ($room->get_room_id() !== null); + } + + /** + * Get the stored room configuration. + * @return null|matrix_rooms + */ + public function get_room_configuration(): ?matrix_rooms { + if ($this->room === null) { + $this->room = matrix_rooms::load_by_processor_id($this->processor->get_id()); + } + + return $this->room; + } + + /** + * Return the current room id. + * + * @return string|null + */ + public function get_room_id(): ?string { + return $this->get_room_configuration()?->get_room_id(); } /** @@ -68,28 +150,30 @@ class communication_feature implements public function create_members(array $userids): void { $addedmembers = []; + // This API requiures the create_user feature. + $this->matrixapi->require_feature(create_user_feature::class); + foreach ($userids as $userid) { $user = \core_user::get_user($userid); $userfullname = fullname($user); // Proceed if we have a user's full name and email to work with. if (!empty($user->email) && !empty($userfullname)) { - $json = [ - 'displayname' => $userfullname, - 'threepids' => [(object)[ - 'medium' => 'email', - 'address' => $user->email - ]], - 'external_ids' => [] - ]; - $qualifiedmuid = matrix_user_manager::get_formatted_matrix_userid($user->username); // First create user in matrix. - $response = $this->eventmanager->request($json)->put($this->eventmanager->get_create_user_endpoint($qualifiedmuid)); - $response = json_decode($response->getBody()); + $response = $this->matrixapi->create_user( + userid: $qualifiedmuid, + displayname: $userfullname, + threepids: [(object)[ + 'medium' => 'email', + 'address' => $user->email + ]], + externalids: [], + ); + $body = json_decode($response->getBody()); - if (!empty($matrixuserid = $response->name)) { + if (!empty($matrixuserid = $body->name)) { // Then create matrix user id in moodle. matrix_user_manager::set_matrix_userid_in_moodle($userid, $qualifiedmuid); if ($this->add_registered_matrix_user_to_room($matrixuserid)) { @@ -100,7 +184,7 @@ class communication_feature implements } // Mark then users as synced for the added members. - $this->communication->mark_users_as_synced($addedmembers); + $this->processor->mark_users_as_synced($addedmembers); } /** @@ -125,7 +209,7 @@ class communication_feature implements } // Mark then users as synced for the added members. - $this->communication->mark_users_as_synced($addedmembers); + $this->processor->mark_users_as_synced($addedmembers); // Create Matrix users. if (count($unregisteredmembers) > 0) { @@ -139,20 +223,25 @@ class communication_feature implements * @param string $matrixuserid Registered matrix user id */ private function add_registered_matrix_user_to_room(string $matrixuserid): bool { - if (!$this->check_room_membership($matrixuserid)) { - $json = ['user_id' => $matrixuserid]; - $headers = ['Content-Type' => 'application/json']; + // Require the invite_member_to_room API feature. + $this->matrixapi->require_feature(invite_member_to_room_feature::class); - $response = $this->eventmanager->request( - $json, - $headers - )->post( - $this->eventmanager->get_room_membership_join_endpoint() + if (!$this->check_room_membership($matrixuserid)) { + $response = $this->matrixapi->invite_member_to_room( + $this->get_room_id(), + $matrixuserid, ); - $response = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR); - if (!empty($roomid = $response->room_id) && $roomid === $this->eventmanager->roomid) { - return true; + + $body = self::get_body($response); + if (empty($body->room_id)) { + return false; } + + if ($body->room_id !== $this->get_room_id()) { + return false; + } + + return true; } return false; } @@ -163,6 +252,9 @@ class communication_feature implements * @param array $userids The Moodle user ids to remove */ public function remove_members_from_room(array $userids): void { + // This API requiures the remove_members_from_room feature. + $this->matrixapi->require_feature(remove_member_from_room_feature::class); + $membersremoved = []; foreach ($userids as $userid) { @@ -170,8 +262,8 @@ class communication_feature implements $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($userid); // Check if user is the room admin and halt removal of this user. - $matrixroomdata = $this->eventmanager->request()->get($this->eventmanager->get_room_info_endpoint()); - $matrixroomdata = json_decode($matrixroomdata->getBody(), false, 512, JSON_THROW_ON_ERROR); + $response = $this->matrixapi->get_room_info($this->get_room_id()); + $matrixroomdata = self::get_body($response); $roomadmin = $matrixroomdata->creator; $isadmin = $matrixuserid === $roomadmin; @@ -179,20 +271,16 @@ class communication_feature implements !$isadmin && $matrixuserid && $this->check_user_exists($matrixuserid) && $this->check_room_membership($matrixuserid) ) { - $json = ['user_id' => $matrixuserid]; - $headers = ['Content-Type' => 'application/json']; - $this->eventmanager->request( - $json, - $headers - )->post( - $this->eventmanager->get_room_membership_kick_endpoint() + $this->matrixapi->remove_member_from_room( + $this->get_room_id(), + $matrixuserid, ); $membersremoved[] = $userid; } } - $this->communication->delete_instance_user_mapping($membersremoved); + $this->processor->delete_instance_user_mapping($membersremoved); } /** @@ -203,10 +291,13 @@ class communication_feature implements * @return bool */ public function check_user_exists(string $matrixuserid): bool { - $response = $this->eventmanager->request([], [], false)->get($this->eventmanager->get_user_info_endpoint($matrixuserid)); - $response = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR); + // This API requires the get_user_info feature. + $this->matrixapi->require_feature(get_user_info_feature::class); - return isset($response->name); + $response = $this->matrixapi->get_user_info($matrixuserid); + $body = self::get_body($response); + + return isset($body->name); } /** @@ -217,68 +308,88 @@ class communication_feature implements * @return bool */ public function check_room_membership(string $matrixuserid): bool { - $response = $this->eventmanager->request([], [], false)->get($this->eventmanager->get_room_membership_joined_endpoint()); - $response = json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR); + // This API requires the get_room_members feature. + $this->matrixapi->require_feature(get_room_members_feature::class); + + $response = $this->matrixapi->get_room_members($this->get_room_id()); + $body = self::get_body($response); // Check user id is in the returned room member ids. - return isset($response['joined']) && array_key_exists($matrixuserid, $response['joined']); + return isset($body->joined) && array_key_exists($matrixuserid, (array) $body->joined); } + /** + * Create a room based on the data in the communication instance. + * + * @return bool + */ public function create_chat_room(): bool { - if ($this->matrixrooms->room_record_exists() && $this->matrixrooms->get_matrix_room_id()) { + if ($this->remote_room_exists()) { + // A room already exists. Update it instead. return $this->update_chat_room(); } - // Create a new room. - $json = [ - 'name' => $this->communication->get_room_name(), - 'visibility' => 'private', - 'preset' => 'private_chat', - 'initial_state' => [], - ]; - // Set the room topic if set. - if (!empty($matrixroomtopic = $this->matrixrooms->get_matrix_room_topic())) { - $json['topic'] = $matrixroomtopic; + // This method requires the create_room API feature. + $this->matrixapi->require_feature(create_room_feature::class); + + $room = $this->get_room_configuration(); + + $response = $this->matrixapi->create_room( + name: $this->processor->get_room_name(), + visibility: 'private', + preset: 'private_chat', + initialstate: [], + options: [ + 'topic' => $room->get_topic(), + ], + ); + + $response = self::get_body($response); + + if (empty($response->room_id)) { + throw new \moodle_exception( + 'Unable to determine ID of matrix room', + ); } - $response = $this->eventmanager->request($json)->post($this->eventmanager->get_create_room_endpoint()); - $response = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR); + // Update our record of the matrix room_id. + $room->update_room_record( + roomid: $response->room_id, + ); - // Check if room was created. - if (!empty($roomid = $response->room_id)) { - if ($this->matrixrooms->room_record_exists()) { - $this->matrixrooms->update_matrix_room_record($roomid, $matrixroomtopic); - } else { - $this->matrixrooms->create_matrix_room_record($this->communication->get_id(), $roomid, $matrixroomtopic); - } - $this->eventmanager->roomid = $roomid; - $this->update_room_avatar(); - return true; - } - - return false; + // Update the room avatar. + $this->update_room_avatar(); + return true; } public function update_chat_room(): bool { - if (!$this->matrixrooms->room_record_exists()) { + if (!$this->remote_room_exists()) { + // No room exists. Create it instead. return $this->create_chat_room(); } + $this->matrixapi->require_features([ + get_room_info_feature::class, + update_room_name_feature::class, + update_room_topic_feature::class, + ]); + // Get room data. - $matrixroomdata = $this->eventmanager->request()->get($this->eventmanager->get_room_info_endpoint()); - $matrixroomdata = json_decode($matrixroomdata->getBody(), false, 512, JSON_THROW_ON_ERROR); + $response = $this->matrixapi->get_room_info($this->get_room_id()); + $remoteroomdata = self::get_body($response); // Update the room name when it's updated from the form. - if ($matrixroomdata->name !== $this->communication->get_room_name()) { - $json = ['name' => $this->communication->get_room_name()]; - $this->eventmanager->request($json)->put($this->eventmanager->get_update_room_name_endpoint()); + if ($remoteroomdata->name !== $this->processor->get_room_name()) { + $this->matrixapi->update_room_name($this->get_room_id(), $this->processor->get_room_name()); } // Update the room topic if set. - if (!empty($matrixroomtopic = $this->matrixrooms->get_matrix_room_topic())) { - $json = ['topic' => $matrixroomtopic]; - $this->eventmanager->request($json)->put($this->eventmanager->get_update_room_topic_endpoint()); - $this->matrixrooms->update_matrix_room_record($this->matrixrooms->get_matrix_room_id(), $matrixroomtopic); + $localroomdata = $this->get_room_configuration(); + if ($remoteroomdata->topic !== $localroomdata->get_topic()) { + $this->matrixapi->update_room_topic( + roomid: $localroomdata->get_room_id(), + topic: $localroomdata->get_topic(), + ); } // Update room avatar. @@ -288,60 +399,81 @@ class communication_feature implements } public function delete_chat_room(): bool { - return $this->matrixrooms->delete_matrix_room_record(); + $this->get_room_configuration()->delete_room_record(); + $this->room = null; + + return true; } /** * Update the room avatar when an instance image is added or updated. */ public function update_room_avatar(): void { + // Either of the following features of the remote API are required. + $this->matrixapi->require_features([ + upload_content_feature::class, + update_room_avatar_feature::class, + ]); + // Check if we have an avatar that needs to be synced. - if (!$this->communication->is_avatar_synced()) { + if ($this->processor->is_avatar_synced()) { + return; + } - $instanceimage = $this->communication->get_avatar(); - $contenturi = null; + $instanceimage = $this->processor->get_avatar(); + $contenturi = null; - // If avatar is set for the instance, upload to Matrix. Otherwise, leave null for unsetting. - if (!empty($instanceimage)) { - $contenturi = $this->eventmanager->upload_matrix_content($instanceimage); - } + // If avatar is set for the instance, upload to Matrix. Otherwise, leave null for unsetting. + if (!empty($instanceimage)) { + // First upload the content. + $response = $this->matrixapi->upload_content($instanceimage); + $body = self::get_body($response); + $contenturi = $body->content_uri; - $response = $this->eventmanager->request(['url' => $contenturi], [], false)->put( - $this->eventmanager->get_update_avatar_endpoint()); + // Now update the room avatar. + $response = $this->matrixapi->update_room_avatar($this->get_room_id(), $contenturi); // Indicate the avatar has been synced if it was successfully set with Matrix. if ($response->getReasonPhrase() === 'OK') { - $this->communication->set_avatar_synced_flag(true); + $this->processor->set_avatar_synced_flag(true); } } } public function get_chat_room_url(): ?string { - // Check for room record in Moodle and that it exists in Matrix. - if (!$this->matrixrooms->room_record_exists() || !$this->matrixrooms->get_matrix_room_id()) { + if (!$this->get_room_id()) { + // We don't have a room id for this record. return null; } - return $this->eventmanager->matrixwebclienturl . '#/room/' . $this->matrixrooms->get_matrix_room_id(); + return sprintf( + "%s#/room/%s", + $this->webclienturl, + $this->get_room_id(), + ); } public function save_form_data(\stdClass $instance): void { $matrixroomtopic = $instance->matrixroomtopic ?? null; - if ($this->matrixrooms->room_record_exists()) { - $this->matrixrooms->update_matrix_room_record($this->matrixrooms->get_matrix_room_id(), $matrixroomtopic); + $room = $this->get_room_configuration(); + + if ($room) { + $room->update_room_record( + topic: $matrixroomtopic, + ); } else { - // Create the record with empty room id as we don't have it yet. - $this->matrixrooms->create_matrix_room_record( - $this->communication->get_id(), - $this->matrixrooms->get_matrix_room_id(), - $matrixroomtopic, + $this->room = matrix_rooms::create_room_record( + processorid: $this->processor->get_id(), + topic: $matrixroomtopic, ); } } public function set_form_data(\stdClass $instance): void { - if (!empty($instance->id) && !empty($this->communication->get_id())) { - $instance->matrixroomtopic = $this->matrixrooms->get_matrix_room_topic(); + if (!empty($instance->id) && !empty($this->processor->get_id())) { + if ($this->room_exists()) { + $instance->matrixroomtopic = $this->get_room_configuration()->get_topic(); + } } } @@ -353,4 +485,16 @@ class communication_feature implements $mform->addHelpButton('matrixroomtopic', 'matrixroomtopic', 'communication_matrix'); $mform->setType('matrixroomtopic', PARAM_TEXT); } + + /** + * Get the body of a response as a stdClass. + * + * @param Response $response + * @return stdClass + */ + public static function get_body(Response $response): stdClass { + $body = $response->getBody(); + + return json_decode($body, false, 512, JSON_THROW_ON_ERROR); + } } diff --git a/communication/provider/matrix/classes/local/command.php b/communication/provider/matrix/classes/local/command.php new file mode 100644 index 00000000000..cffcab1db25 --- /dev/null +++ b/communication/provider/matrix/classes/local/command.php @@ -0,0 +1,189 @@ +. + +namespace communication_matrix\local; + +use communication_matrix\matrix_client; +use GuzzleHttp\Psr7\Request; +use OutOfRangeException; + +/** + * A command to be sent to the Matrix server. + * + * This class is a wrapper around the PSR-7 Request Interface implementation provided by Guzzle. + * + * It takes a set of common parameters and configurations and turns them into a Request that can be called against a live server. + * + * @package communication_matrix + * @copyright Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class command extends Request { + /** @var array $command The raw command data */ + /** @var array|null $params The parameters passed into the command */ + /** @var bool $sendasjson Whether to send params as JSON */ + /** @var bool $requireauthorization Whether authorization is required for this request */ + /** @var bool $ignorehttperrors Whether to ignore HTTP Errors */ + /** @var array $query Any query parameters to set on the URL */ + + /** @var array|null Any parameters not used in the URI which are to be passed to the server via body or query params */ + protected array $remainingparams = []; + + /** + * Create a new Command. + * + * @param matrix_client $client The URL for this method + * @param string $method (GET|POST|PUT|DELETE) + * @param string $endpoint The URL + * @param array $params Any parameters to pass + * @param array $query Any query parameters to set on the URL + * @param bool $ignorehttperrors Whether to ignore HTTP Errors + * @param bool $requireauthorization Whether authorization is required for this request + * @param bool $sendasjson Whether to send params as JSON + */ + public function __construct( + protected matrix_client $client, + string $method, + string $endpoint, + protected array $params = [], + protected array $query = [], + protected bool $ignorehttperrors = false, + protected bool $requireauthorization = true, + protected bool $sendasjson = true, + ) { + foreach ($params as $name => $value) { + if ($name[0] === ':') { + if (preg_match("/{$name}\\b/", $endpoint) !== 1) { + throw new OutOfRangeException("Parameter not found in URL '{$name}'"); + } + + $endpoint = preg_replace("/{$name}\\b/", urlencode($value), $endpoint); + unset($params[$name]); + } + } + + // Store the modified params. + $this->remainingparams = $params; + + if (str_contains($endpoint, '/:')) { + throw new OutOfRangeException("URL contains untranslated parameters '{$endpoint}'"); + } + + // Process the required headers. + $headers = [ + 'Content-Type' => 'application/json', + ]; + + if ($this->require_authorization()) { + $headers['Authorization'] = 'Bearer ' . $this->client->get_token(); + } + + // Construct the final request. + parent::__construct( + $method, + $this->get_url($endpoint), + $headers, + ); + } + + /** + * Get the URL of the endpoint on the server. + * + * @param string $endpoint + * @return string + */ + protected function get_url(string $endpoint): string { + return sprintf( + "%s/%s", + $this->client->get_server_url(), + $endpoint, + ); + } + + /** + * Get all parameters, including those set in the URL. + * + * @return array + */ + public function get_all_params(): array { + return $this->params; + } + + /** + * Get the parameters provided to the command which are not used in the URL. + * + * These are typically passed to the server as query or body parameters instead. + * + * @return array + */ + public function get_remaining_params(): array { + return $this->remainingparams; + } + + /** + * Get the Guzzle options to pass into the request. + * + * @return array + */ + public function get_options(): array { + $options = []; + + if (count($this->query)) { + $options['query'] = $this->query; + } + + if ($this->should_send_params_as_json()) { + $options['json'] = $this->get_remaining_params(); + } + + if ($this->should_ignore_http_errors()) { + $options['http_errors'] = false; + } + + return $options; + } + + /** + * Whether authorization is required. + * + * Based on the 'authorization' attribute set in a raw command. + * + * @return bool + */ + public function require_authorization(): bool { + return $this->requireauthorization; + } + + /** + * Whether to ignore http errors on the response. + * + * Based on the 'ignore_http_errors' attribute set in a raw command. + * + * @return bool + */ + public function should_ignore_http_errors(): bool { + return $this->ignorehttperrors; + } + + /** + * Whether to send remaining parameters as JSON. + * + * @return bool + */ + public function should_send_params_as_json(): bool { + return $this->sendasjson; + } +} diff --git a/communication/provider/matrix/classes/local/spec/features/matrix/create_room_v3.php b/communication/provider/matrix/classes/local/spec/features/matrix/create_room_v3.php new file mode 100644 index 00000000000..57980c8d121 --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/matrix/create_room_v3.php @@ -0,0 +1,79 @@ +. + +namespace communication_matrix\local\spec\features\matrix; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Matrix API feature for room creation. + * + * https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3createroom + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait create_room_v3 { + + /** + * Create a new room. + * + * @param string $name The room name + * @param null|string $visibility The room visibility + * @param null|string $preset The preset to use + * @param null|array $initialstate Initial state variables + * @param array $options Any additional options + * @return Response + */ + public function create_room( + string $name, + ?string $visibility = null, + ?string $preset = null, + ?array $initialstate = null, + array $options = [], + ): Response { + $params = [ + 'name' => $name, + ]; + + if ($visibility !== null) { + $params['visibility'] = $visibility; + } + + if ($preset !== null) { + $params['preset'] = $preset; + } + + if ($initialstate !== null) { + $params['initial_state'] = $initialstate; + } + + if (array_key_exists('topic', $options)) { + $params['topic'] = $options['topic'] ?? ''; + } + + return $this->execute(new command( + $this, + method: 'POST', + endpoint: '_matrix/client/v3/createRoom', + params: $params, + )); + } +} diff --git a/communication/provider/matrix/classes/local/spec/features/matrix/get_room_members_v3.php b/communication/provider/matrix/classes/local/spec/features/matrix/get_room_members_v3.php new file mode 100644 index 00000000000..60da91df446 --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/matrix/get_room_members_v3.php @@ -0,0 +1,53 @@ +. + +namespace communication_matrix\local\spec\features\matrix; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Matrix API feature to fetch a list of room members. + * + * https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomidjoined_members + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait get_room_members_v3 { + + /** + * Get a list of room members. + * + * @param string $roomid The room ID + * @return Response + */ + public function get_room_members(string $roomid): Response { + $params = [ + ':roomid' => $roomid, + ]; + + return $this->execute(new command( + $this, + method: 'GET', + endpoint: '_matrix/client/v3/rooms/:roomid/joined_members', + params: $params, + )); + } +} diff --git a/communication/provider/matrix/classes/local/spec/features/matrix/remove_member_from_room_v3.php b/communication/provider/matrix/classes/local/spec/features/matrix/remove_member_from_room_v3.php new file mode 100644 index 00000000000..89e6b89cc9d --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/matrix/remove_member_from_room_v3.php @@ -0,0 +1,58 @@ +. + +namespace communication_matrix\local\spec\features\matrix; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Matrix API feature to remove a member from a room. + * + * https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidkick + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait remove_member_from_room_v3 { + + /** + * Remove a member from a room. + * + * @param string $roomid The roomid to remove from + * @param string $userid The member to remove + * @return Response + */ + public function remove_member_from_room( + string $roomid, + string $userid, + ): Response { + $params = [ + ':roomid' => $roomid, + 'user_id' => $userid, + ]; + + return $this->execute(new command( + $this, + method: 'POST', + endpoint: '_matrix/client/v3/rooms/:roomid/kick', + params: $params, + )); + } +} diff --git a/communication/provider/matrix/classes/local/spec/features/matrix/update_room_avatar_v3.php b/communication/provider/matrix/classes/local/spec/features/matrix/update_room_avatar_v3.php new file mode 100644 index 00000000000..1df828f793e --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/matrix/update_room_avatar_v3.php @@ -0,0 +1,60 @@ +. + +namespace communication_matrix\local\spec\features\matrix; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Matrix API feature to update a room avatar. + * + * https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait update_room_avatar_v3 { + + /** + * Set the avatar for a room to the specified URL. + * + * @param string $roomid The roomid to set for + * @param null|string $avatarurl The mxc URL to use + * @return Response + */ + public function update_room_avatar( + string $roomid, + ?string $avatarurl, + ): Response { + $params = [ + ':roomid' => $roomid, + 'url' => $avatarurl, + ]; + + return $this->execute(new command( + $this, + method: 'PUT', + endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.avatar', + ignorehttperrors: true, + params: $params, + )); + } + +} diff --git a/communication/provider/matrix/classes/local/spec/features/matrix/update_room_name_v3.php b/communication/provider/matrix/classes/local/spec/features/matrix/update_room_name_v3.php new file mode 100644 index 00000000000..e7235ed9a89 --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/matrix/update_room_name_v3.php @@ -0,0 +1,55 @@ +. + +namespace communication_matrix\local\spec\features\matrix; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Matrix API feature to update a room name. + * + * https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait update_room_name_v3 { + + /** + * Set the name for a room. + * + * @param string $roomid + * @param string $name + * @return Response + */ + public function update_room_name(string $roomid, string $name): Response { + $params = [ + ':roomid' => $roomid, + 'name' => $name, + ]; + + return $this->execute(new command( + $this, + method: 'PUT', + endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.name', + params: $params, + )); + } +} diff --git a/communication/provider/matrix/classes/local/spec/features/matrix/update_room_topic_v3.php b/communication/provider/matrix/classes/local/spec/features/matrix/update_room_topic_v3.php new file mode 100644 index 00000000000..c68f45316a1 --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/matrix/update_room_topic_v3.php @@ -0,0 +1,56 @@ +. + +namespace communication_matrix\local\spec\features\matrix; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Matrix API feature to update a room topic. + * + * https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait update_room_topic_v3 { + + /** + * Set the topic for a room. + * + * @param string $roomid + * @param string $topic + * @return Response + */ + public function update_room_topic(string $roomid, string $topic): Response { + $params = [ + ':roomid' => $roomid, + 'topic' => $topic, + ]; + + return $this->execute(new command( + $this, + method: 'PUT', + endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.topic', + params: $params, + )); + + } +} diff --git a/communication/provider/matrix/classes/local/spec/features/matrix/upload_content_v3.php b/communication/provider/matrix/classes/local/spec/features/matrix/upload_content_v3.php new file mode 100644 index 00000000000..962553e9592 --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/matrix/upload_content_v3.php @@ -0,0 +1,66 @@ +. + +namespace communication_matrix\local\spec\features\matrix; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Psr7\Utils; + +/** + * Matrix API feature to upload content. + * + * https://spec.matrix.org/v1.1/client-server-api/#post_matrixmediav3upload + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait upload_content_v3 { + + /** + * Upload the content in the matrix/synapse server. + * + * @param null|\stored_file $content The content to be uploaded + * @return Response + */ + public function upload_content( + ?\stored_file $content, + ): Response { + $query = []; + if ($content) { + $query['filename'] = $content->get_filename(); + } + + $command = new command( + $this, + method: 'POST', + endpoint: '_matrix/media/v3/upload', + sendasjson: false, + query: $query, + ); + + if ($content) { + // Add the content-type, and header. + $command = $command->withHeader('Content-Type', $content->get_mimetype()); + $command = $command->withBody(Utils::streamFor($content->get_content())); + } + + return $this->execute($command); + } +} diff --git a/communication/provider/matrix/classes/local/spec/features/synapse/create_user_v2.php b/communication/provider/matrix/classes/local/spec/features/synapse/create_user_v2.php new file mode 100644 index 00000000000..9d89a1a857c --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/synapse/create_user_v2.php @@ -0,0 +1,67 @@ +. + +namespace communication_matrix\local\spec\features\synapse; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Synapse API feature for creating a user. + * + * https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait create_user_v2 { + + /** + * Create a new user. + * + * @param string $userid The Matrix user id. + * @param string $displayname The visible name of the user + * @param array $threepids The third-party identifiers of the user. + * @param null|array $externalids + */ + public function create_user( + string $userid, + string $displayname, + array $threepids, + ?array $externalids = null, + ): Response { + $params = [ + ':userid' => $userid, + 'displayname' => $displayname, + 'threepids' => $threepids, + ]; + + if ($externalids !== null) { + $params['externalids'] = $externalids; + } + + return $this->execute(new command( + $this, + method: 'PUT', + endpoint: '_synapse/admin/v2/users/:userid', + params: $params, + )); + } + +} diff --git a/communication/provider/matrix/classes/local/spec/features/synapse/get_room_info_v1.php b/communication/provider/matrix/classes/local/spec/features/synapse/get_room_info_v1.php new file mode 100644 index 00000000000..4903c4d632d --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/synapse/get_room_info_v1.php @@ -0,0 +1,51 @@ +. + +namespace communication_matrix\local\spec\features\synapse; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Synapse API feature for fetching room info. + * + * https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-details-api + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait get_room_info_v1 { + + /** + * Get room info. + * + * @param string $roomid + * @return Response + */ + public function get_room_info(string $roomid): Response { + return $this->execute(new command( + $this, + method: 'GET', + endpoint: '_synapse/admin/v1/rooms/:roomid', + params: [ + ':roomid' => $roomid, + ], + )); + } +} diff --git a/communication/provider/matrix/classes/local/spec/features/synapse/get_user_info_v2.php b/communication/provider/matrix/classes/local/spec/features/synapse/get_user_info_v2.php new file mode 100644 index 00000000000..2b73867c90d --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/synapse/get_user_info_v2.php @@ -0,0 +1,53 @@ +. + +namespace communication_matrix\local\spec\features\synapse; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Synapse API feature for fetching info about a user. + * + * https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#query-user-account + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait get_user_info_v2 { + + /** + * Get user info. + * + * @param string $userid + * @return Response + */ + public function get_user_info(string $userid): Response { + return $this->execute(new command( + $this, + method: 'GET', + endpoint: '_synapse/admin/v2/users/:userid', + ignorehttperrors: true, + params: [ + ':userid' => $userid, + ], + )); + } + +} diff --git a/communication/provider/matrix/classes/local/spec/features/synapse/invite_member_to_room_v1.php b/communication/provider/matrix/classes/local/spec/features/synapse/invite_member_to_room_v1.php new file mode 100644 index 00000000000..bce95b7eb4a --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/synapse/invite_member_to_room_v1.php @@ -0,0 +1,57 @@ +. + +namespace communication_matrix\local\spec\features\synapse; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Synapse API feature to invite a user into a room. + * + * https://matrix-org.github.io/synapse/latest/admin_api/room_membership.html#edit-room-membership-api + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. + */ +trait invite_member_to_room_v1 { + + /** + * Join a user to a room. + * + * Note: This joins the user, and does not invite them. + * + * @param string $roomid + * @param string $userid + * @return Response + */ + public function invite_member_to_room(string $roomid, string $userid): Response { + $params = [ + ':roomid' => $roomid, + 'user_id' => $userid, + ]; + + return $this->execute(new command( + $this, + method: 'POST', + endpoint: '_synapse/admin/v1/join/:roomid', + params: $params, + )); + } +} diff --git a/communication/provider/matrix/classes/local/spec/v1p1.php b/communication/provider/matrix/classes/local/spec/v1p1.php new file mode 100644 index 00000000000..e75b43343aa --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/v1p1.php @@ -0,0 +1,45 @@ +. + +namespace communication_matrix\local\spec; + +/** + * Matrix API to support version v1.1 of the Matrix specification. + * + * https://spec.matrix.org/v1.1/client-server-api/ + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class v1p1 extends \communication_matrix\matrix_client { + // Use the standard matrix API for these features. + use features\matrix\create_room_v3; + use features\matrix\get_room_members_v3; + use features\matrix\remove_member_from_room_v3; + use features\matrix\update_room_avatar_v3; + use features\matrix\update_room_name_v3; + use features\matrix\update_room_topic_v3; + use features\matrix\upload_content_v3; + + // We use the Synapse API here because it can invite users to a room without requiring them to accept the invite. + use features\synapse\invite_member_to_room_v1; + + // User information and creation is a server-specific feature. + use features\synapse\get_user_info_v2; + use features\synapse\create_user_v2; + use features\synapse\get_room_info_v1; +} diff --git a/communication/provider/matrix/classes/local/spec/v1p2.php b/communication/provider/matrix/classes/local/spec/v1p2.php new file mode 100644 index 00000000000..838464f615b --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/v1p2.php @@ -0,0 +1,30 @@ +. + +namespace communication_matrix\local\spec; + +/** + * Matrix API to support version v1.2 of the Matrix specification. + * + * https://spec.matrix.org/v1.2/client-server-api/ + * https://spec.matrix.org/v1.2/changelog/#api-changes + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class v1p2 extends v1p1 { +} diff --git a/communication/provider/matrix/classes/local/spec/v1p3.php b/communication/provider/matrix/classes/local/spec/v1p3.php new file mode 100644 index 00000000000..70399fe85cf --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/v1p3.php @@ -0,0 +1,30 @@ +. + +namespace communication_matrix\local\spec; + +/** + * Matrix API to support version v1.3 of the Matrix specification. + * + * https://spec.matrix.org/v1.3/client-server-api/ + * https://spec.matrix.org/v1.3/changelog/#api-changes + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class v1p3 extends v1p2 { +} diff --git a/communication/provider/matrix/classes/local/spec/v1p4.php b/communication/provider/matrix/classes/local/spec/v1p4.php new file mode 100644 index 00000000000..9fd015f10ca --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/v1p4.php @@ -0,0 +1,30 @@ +. + +namespace communication_matrix\local\spec; + +/** + * Matrix API to support version v1.4 of the Matrix specification. + * + * https://spec.matrix.org/v1.4/client-server-api/ + * https://spec.matrix.org/v1.4/changelog/#api-changes + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class v1p4 extends v1p3 { +} diff --git a/communication/provider/matrix/classes/local/spec/v1p5.php b/communication/provider/matrix/classes/local/spec/v1p5.php new file mode 100644 index 00000000000..844a84f182d --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/v1p5.php @@ -0,0 +1,30 @@ +. + +namespace communication_matrix\local\spec; + +/** + * Matrix API to support version v1.5 of the Matrix specification. + * + * https://spec.matrix.org/v1.5/client-server-api/ + * https://spec.matrix.org/v1.5/changelog/#api-changes + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class v1p5 extends v1p4 { +} diff --git a/communication/provider/matrix/classes/local/spec/v1p6.php b/communication/provider/matrix/classes/local/spec/v1p6.php new file mode 100644 index 00000000000..2b5fb61f0dc --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/v1p6.php @@ -0,0 +1,30 @@ +. + +namespace communication_matrix\local\spec; + +/** + * Matrix API to support version v1.6 of the Matrix specification. + * + * https://spec.matrix.org/v1.6/client-server-api/ + * https://spec.matrix.org/v1.6/changelog/#api-changes + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class v1p6 extends v1p5 { +} diff --git a/communication/provider/matrix/classes/local/spec/v1p7.php b/communication/provider/matrix/classes/local/spec/v1p7.php new file mode 100644 index 00000000000..3e2012500b3 --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/v1p7.php @@ -0,0 +1,33 @@ +. + +namespace communication_matrix\local\spec; + +/** + * Matrix API to support version v1.7 of the Matrix specification. + * + * https://spec.matrix.org/v1.7/client-server-api/ + * https://spec.matrix.org/v1.7/changelog/#api-changes + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class v1p7 extends v1p6 { + // Note: A new Content Upload API was introduced, but it doesn't benefit us in any way. + // See details in the spec: + // https://github.com/matrix-org/matrix-spec-proposals/pull/2246. +} diff --git a/communication/provider/matrix/classes/matrix_client.php b/communication/provider/matrix/classes/matrix_client.php new file mode 100644 index 00000000000..206fe08cff6 --- /dev/null +++ b/communication/provider/matrix/classes/matrix_client.php @@ -0,0 +1,341 @@ +. + +namespace communication_matrix; + +use communication_matrix\local\command; +use core\http_client; +use DirectoryIterator; +use Exception; +use GuzzleHttp\Psr7\Response; + +/** + * The abstract class for a versioned API client for Matrix. + * + * Matrix uses a versioned API, and a handshake occurs between the Client (Moodle) and server, to determine the APIs available. + * + * This client represents a version-less API client. + * Versions are implemented by combining the various features into a versionedclass. + * See v1p1 for example. + * + * @package communication_matrix + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class matrix_client { + + /** @var string $serverurl The URL of the home server */ + /** @var string $accesstoken The access token of the matrix server */ + + /** @var http_client|null The client to use */ + protected static http_client|null $client = null; + + /** + * Matrix events constructor to get the room id and refresh token usage if required. + * + * @param string $serverurl The URL of the API server + * @param string $accesstoken The admin access token + */ + protected function __construct( + protected string $serverurl, + protected string $accesstoken, + ) { + } + + /** + * Return the versioned instance of the API. + * + * @param string $serverurl The URL of the API server + * @param string $accesstoken The admin access token to use + * @return matrix_client + */ + public static function instance( + string $serverurl, + string $accesstoken, + ): matrix_client { + // Fetch the list of supported API versions. + $clientversions = self::get_supported_versions(); + + // Fetch the supported versions from the server. + $serversupports = self::query_server_supports($serverurl); + $serverversions = $serversupports->versions; + + // Calculate the intersections and sort to determine the highest combined version. + $versions = array_intersect($clientversions, $serverversions); + if (count($versions) === 0) { + // No versions in common. + throw new \moodle_exception('No supported Matrix API versions found.'); + } + asort($versions); + $version = array_key_last($versions); + + $classname = \communication_matrix\local\spec::class . '\\' . $version; + + return new $classname( + $serverurl, + $accesstoken, + ); + } + + /** + * Determine if the API supports a feature. + * + * If an Array is provided, this will return true if any of the specified features is implemented. + * + * @param string[]|string $feature The feature to check. This is in the form of a namespaced class. + * @return bool + */ + public function implements_feature(array|string $feature): bool { + if (is_array($feature)) { + foreach ($feature as $thisfeature) { + if ($this->implements_feature($thisfeature)) { + return true; + } + } + + // None of the features are implemented in this API version. + return false; + } + + return in_array($feature, $this->get_supported_features()); + } + + /** + * Get a list of the features supported by this client. + * + * @return string[] + */ + public function get_supported_features(): array { + $features = []; + $class = static::class; + do { + $features = array_merge($features, class_uses($class)); + $class = get_parent_class($class); + } while ($class); + + return $features; + } + + /** + * Require that the API supports a feature. + * + * If an Array is provided, this is treated as a require any of the features. + * + * @param string[]|string $feature The feature to test + * @throws \moodle_exception + */ + public function require_feature(array|string $feature): void { + if (!$this->implements_feature($feature)) { + if (is_array($feature)) { + $features = implode(', ', $feature); + throw new \moodle_exception( + "None of the possible feature are implemented in this Matrix Client: '{$features}'" + ); + } + throw new \moodle_exception("The requested feature is not implemented in this Matrix Client: '{$feature}'"); + } + } + + /** + * Require that the API supports a list of features. + * + * All features specified will be required. + * + * If an array is provided as one of the features, any of the items in the nested array will be required. + * + * @param string[]|array[] $features The list of features required + * + * Here is an example usage: + * + * $matrixapi->require_features([ + * + * \communication_matrix\local\spec\features\create_room::class, + * [ + * \communication_matrix\local\spec\features\get_room_info_v1::class, + * \communication_matrix\local\spec\features\get_room_info_v2::class, + * ] + * ]) + * + */ + public function require_features(array $features): void { + array_walk($features, [$this, 'require_feature']); + } + + /** + * Get the URL of the server. + * + * @return string + */ + public function get_server_url(): string { + return $this->serverurl; + } + + /** + * Query the supported versions, and any unstable features, from the server. + * + * Servers must implement the client versions API described here: + * - https://spec.matrix.org/latest/client-server-api/#get_matrixclientversions + * + * @param string $serverurl The server base + * @return \stdClass The list of supported versions and a list of enabled unstable features + */ + protected static function query_server_supports(string $serverurl): \stdClass { + // Attempt to return from the cache first. + $cache = \cache::make('communication_matrix', 'serverversions'); + $serverkey = sha1($serverurl); + if ($cache->get($serverkey)) { + return $cache->get($serverkey); + } + + // Not in the cache - fetch and store in the cache. + $client = static::get_http_client(); + $response = $client->get("{$serverurl}/_matrix/client/versions"); + $supportsdata = json_decode( + json: $response->getBody(), + associative: false, + flags: JSON_THROW_ON_ERROR, + ); + + $cache->set($serverkey, $supportsdata); + + return $supportsdata; + } + + /** + * Get the list of supported versions based on the available classes. + * + * @return array + */ + public static function get_supported_versions(): array { + $versions = []; + $iterator = new DirectoryIterator(__DIR__ . '/local/spec'); + foreach ($iterator as $fileinfo) { + if ($fileinfo->isDir()) { + continue; + } + + // Get the classname from the filename. + $classname = substr($fileinfo->getFilename(), 0, -4); + + if (!preg_match('/^v\d+p\d+$/', $classname)) { + // @codeCoverageIgnoreStart + // This file does not fit the format v[MAJOR]p[MINOR]]. + continue; + // @codeCoverageIgnoreEnd + } + + $versions[$classname] = "v" . self::get_version_from_classname($classname); + } + + return $versions; + } + + /** + * Get the current token in use. + * + * @return string + */ + public function get_token(): string { + return $this->accesstoken; + } + + /** + * Helper to fetch the HTTP Client for the instance. + * + * @return \core\http_client + */ + protected function get_client(): \core\http_client { + return static::get_http_client(); + } + + /** + * Helper to fetch the HTTP Client. + * + * @return \core\http_client + */ + protected static function get_http_client(): \core\http_client { + if (static::$client !== null) { + return static::$client; + } + // @codeCoverageIgnoreStart + return new http_client(); + // @codeCoverageIgnoreEnd + } + + /** + * Execute the specified command. + * + * @param command $command + * @return Response + */ + protected function execute( + command $command, + ): Response { + $client = $this->get_client(); + return $client->send( + $command, + $command->get_options(), + ); + } + + /** + * Get the API version of the current instance. + * + * @return string + */ + public function get_version(): string { + $reflect = new \ReflectionClass(static::class); + $classname = $reflect->getShortName(); + return self::get_version_from_classname($classname); + } + + /** + * Normalise an API version from a classname. + * + * @param string $classname The short classname, omitting any namespace or file extension + * @return string The normalised version + */ + protected static function get_version_from_classname(string $classname): string { + $classname = str_replace('v', '', $classname); + $classname = str_replace('p', '.', $classname); + return $classname; + } + + /** + * Check if the API version is at least the specified version. + * + * @param string $minversion The minimum API version required + * @return bool + */ + public function meets_version(string $minversion): bool { + $thisversion = $this->get_version(); + return version_compare($thisversion, $minversion) >= 0; + } + + /** + * Assert that the API version is at least the specified version. + * + * @param string $minversion The minimum API version required + * @throws Exception + */ + public function requires_version(string $minversion): void { + if ($this->meets_version($minversion)) { + return; + } + + throw new \moodle_exception("Matrix API version {$minversion} or higher is required for this command."); + } +} diff --git a/communication/provider/matrix/classes/matrix_events_manager.php b/communication/provider/matrix/classes/matrix_events_manager.php deleted file mode 100644 index 39c64c7480f..00000000000 --- a/communication/provider/matrix/classes/matrix_events_manager.php +++ /dev/null @@ -1,261 +0,0 @@ -. - -namespace communication_matrix; - -use core\http_client; -use stored_file; -use GuzzleHttp\Psr7\Request; - -/** - * Class matrix_endpoint_manager to manage the api endpoints of matrix provider. - * - * @package communication_matrix - * @copyright 2023 Safat Shahin - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class matrix_events_manager { - - /** - * @var string|false|mixed|object $matrixhomeserverurl The URL of the home server - */ - public string $matrixhomeserverurl; - - /** - * @var string $matrixwebclienturl The URL of the web client - */ - public string $matrixwebclienturl; - - /** - * @var string|false|mixed|object $matrixaccesstoken The access token of the matrix server - */ - private string $matrixaccesstoken; - - /** - * @var string $roomid The id of the room from matrix server - */ - public string $roomid; - - /** - * Matrix events constructor to get the room id and refresh token usage if required. - * - * @param string|null $roomid The id of the room from matrix server - */ - public function __construct(?string $roomid = null) { - if (!empty($roomid)) { - $this->roomid = $roomid; - } - - $this->matrixhomeserverurl = get_config('communication_matrix', 'matrixhomeserverurl'); - $this->matrixaccesstoken = get_config('communication_matrix', 'matrixaccesstoken'); - $this->matrixwebclienturl = get_config('communication_matrix', 'matrixelementurl'); - } - - /** - * Get the current token in use. - * - * @return string - */ - public function get_token(): string { - return $this->matrixaccesstoken; - } - - /** - * Get the matrix api endpoint for creating a new room. - * - * @return string - */ - public function get_create_room_endpoint(): string { - return $this->matrixhomeserverurl . '/' . '_matrix/client/r0/createRoom'; - } - - /** - * Get the matrix api endpoint for updating the room topic. - * - * @return string - */ - public function get_update_room_topic_endpoint(): string { - if (!empty($this->roomid)) { - return $this->matrixhomeserverurl . '/' . '_matrix/client/r0/rooms' . - '/' . urlencode($this->roomid) . '/' . 'state/m.room.topic/'; - } - } - - /** - * Get the matrix api endpoint for updating room name. - * - * @return string - */ - public function get_update_room_name_endpoint(): string { - if (!empty($this->roomid)) { - return $this->matrixhomeserverurl . '/' . '_matrix/client/r0/rooms' . - '/' . urlencode($this->roomid) . '/' . 'state/m.room.name/'; - } - } - - /** - * Get matrix api endpoint for getting room information. - * - * @return string - */ - public function get_room_info_endpoint(): string { - if (!empty($this->roomid)) { - return $this->matrixhomeserverurl . '/_synapse/admin/v1/rooms/' . urlencode($this->roomid); - } - } - - /** - * Get delete room endpoint. - * - * @return string - */ - public function get_delete_room_endpoint(): string { - if (!empty($this->roomid)) { - return $this->matrixhomeserverurl . '/' . '_synapse/admin/v1/rooms/' . urlencode($this->roomid); - } - } - - /** - * Get the matrix api endpoint for uploading a content to synapse server. - */ - public function get_upload_content_endpoint(): string { - return $this->matrixhomeserverurl . '/' . '_matrix/media/r0/upload/'; - } - - /** - * Get the matrix api endpoint for updating room avatar. - * - * @return string - */ - public function get_update_avatar_endpoint(): string { - if (!empty($this->roomid)) { - return $this->matrixhomeserverurl . '/' . '_matrix/client/r0/rooms' . - '/' . urlencode($this->roomid) . '/' . 'state/m.room.avatar/'; - } - } - - /** - * Get the members of a room. Useful when performing actions where member needs to exist first. - * - * @return string - */ - public function get_room_membership_joined_endpoint(): string { - if (!empty($this->roomid)) { - return $this->matrixhomeserverurl . '/' . '_matrix/client/r0/rooms' . - '/' . urlencode($this->roomid) . '/' . 'joined_members'; - } - } - - /** - * Get the 'join' room membership endpoint. This adds users to a room. - * - * @return string - */ - public function get_room_membership_join_endpoint(): string { - if (!empty($this->roomid)) { - return $this->matrixhomeserverurl . '/' . '_synapse/admin/v1/join' . '/' . urlencode($this->roomid); - } - } - - /** - * Get the 'kick' room membership endpoint. This removes users from a room. - * - * @return string - */ - public function get_room_membership_kick_endpoint(): string { - if (!empty($this->roomid)) { - return $this->matrixhomeserverurl . '/' . '_matrix/client/r0/rooms' . - '/' . urlencode($this->roomid) . '/' . 'kick'; - } - } - - /** - * Get the matrix api endpoint for creating a new user. - * - * @param string $matrixuserid Matrix user id - * @return string - */ - public function get_create_user_endpoint(string $matrixuserid): string { - return $this->matrixhomeserverurl . '/' . '_synapse/admin/v2/users/' . urlencode($matrixuserid); - } - - /** - * Get the matrix api endpoint for creating a new user. - * - * @param string $matrixuserid Matrix user id - * @return string - */ - public function get_user_info_endpoint(string $matrixuserid): string { - return $this->matrixhomeserverurl . '/' . '_synapse/admin/v2/users/' . urlencode($matrixuserid); - } - - /** - * The http request for the api call. - * - * @param array $jsonarray The array of json - * @param array $headers The array of headers - * @param bool $httperror Enable or disable http error from response - * @return \core\http_client - */ - public function request(array $jsonarray = [], array $headers = [], bool $httperror = true): \core\http_client { - $header = ['Authorization' => 'Bearer ' . $this->get_token()]; - $headers = array_merge($header, $headers); - $response = new \core\http_client([ - 'http_errors' => $httperror, - 'headers' => $headers, - 'json' => $jsonarray, - ]); - return $response; - } - - /** - * Upload the content in the matrix/synapse server. - * - * @param null|stored_file $file The content to be uploaded - * @return string|false - */ - public function upload_matrix_content(?stored_file $file): bool|string { - $headers = [ - 'Authorization' => 'Bearer ' . $this->get_token(), - ]; - $filecontent = null; - $query = []; - - if ($file) { - $filecontent = $file->get_content(); - $headers['Content-Type'] = $file->get_mimetype(); - $query['filename'] = $file->get_filename(); - } - - $client = new http_client(); - $request = new Request( - 'POST', - $this->get_upload_content_endpoint(), - $headers, - $filecontent, - ); - - $response = $client->send($request, [ - 'query' => $query, - ]); - $response = json_decode($response->getBody()); - if ($response) { - return $response->content_uri; - } - return false; - } - -} diff --git a/communication/provider/matrix/classes/matrix_rooms.php b/communication/provider/matrix/classes/matrix_rooms.php index a3b6d74d3dd..2a00f55128d 100644 --- a/communication/provider/matrix/classes/matrix_rooms.php +++ b/communication/provider/matrix/classes/matrix_rooms.php @@ -16,6 +16,8 @@ namespace communication_matrix; +use stdClass; + /** * Class matrix_rooms to manage the updates to the room information in db. * @@ -25,80 +27,101 @@ namespace communication_matrix; */ class matrix_rooms { - /** - * @var \stdClass|null $matrixroomrecord The matrix room record from db - */ - private ?\stdClass $matrixroomrecord = null; + /** @var \stdClass|null $record The matrix room record from db */ + /** + * Load the matrix room record for the supplied processor. + * @param int $processorid + * @return null|self + */ + public static function load_by_processor_id( + int $processorid, + ): ?self { + global $DB; + + $record = $DB->get_record('matrix_rooms', ['commid' => $processorid]); + + if (!$record) { + return null; + } + return new self($record); + } /** * Matrix rooms constructor to load the matrix room information from matrix_rooms table. * - * @param int $commid The id of the communication record + * @param stdClass $record */ - public function __construct(int $commid) { - $this->load_matrix_room_data($commid); - } - - /** - * Get the matrix room data from database. Either get the data object or return false if no data found. - * - * @param int $commid The id of the communication record - */ - public function load_matrix_room_data(int $commid): void { - global $DB; - if ($record = $DB->get_record('matrix_rooms', ['commid' => $commid])) { - $this->matrixroomrecord = $record; - } + private function __construct( + private stdClass $record, + ) { } /** * Create matrix room data. * - * @param int $commid The id of the communication record + * @param int $processorid The id of the communication record + * @param string|null $topic The topic of the room for matrix * @param string|null $roomid The id of the room from matrix - * @param string|null $roomtopic The topic of the room for matrix + * @return self */ - public function create_matrix_room_record( - int $commid, - ?string $roomid, - ?string $roomtopic - ): void { + public static function create_room_record( + int $processorid, + ?string $topic, + ?string $roomid = null, + ): self { global $DB; - $roomrecord = new \stdClass(); - $roomrecord->commid = $commid; - $roomrecord->roomid = $roomid; - $roomrecord->topic = $roomtopic; + + $roomrecord = (object) [ + 'commid' => $processorid, + 'roomid' => $roomid, + 'topic' => $topic, + ]; $roomrecord->id = $DB->insert_record('matrix_rooms', $roomrecord); - $this->matrixroomrecord = $roomrecord; + + return self::load_by_processor_id($processorid); } /** * Update matrix room data. * * @param string|null $roomid The id of the room from matrix - * @param string|null $roomtopic The topic of the room for matrix + * @param string|null $topic The topic of the room for matrix */ - public function update_matrix_room_record(?string $roomid, ?string $roomtopic): void { + public function update_room_record( + ?string $roomid = null, + ?string $topic = null, + ): void { global $DB; - if ($this->room_record_exists()) { - $this->matrixroomrecord->roomid = $roomid; - $this->matrixroomrecord->topic = $roomtopic; - $DB->update_record('matrix_rooms', $this->matrixroomrecord); + + if ($roomid !== null) { + $this->record->roomid = $roomid; } + + if ($topic !== null) { + $this->record->topic = $topic; + } + + $DB->update_record('matrix_rooms', $this->record); } /** * Delete matrix room data. - * - * @return bool */ - public function delete_matrix_room_record(): bool { + public function delete_room_record(): void { global $DB; - if ($this->room_record_exists()) { - return $DB->delete_records('matrix_rooms', ['commid' => $this->matrixroomrecord->commid]); - } - return false; + $DB->delete_records('matrix_rooms', ['commid' => $this->record->commid]); + + unset($this->record); + } + + /** + * Get the processor id. + * + * @return int + */ + public function get_processor_id(): int { + return $this->record->commid; } /** @@ -106,11 +129,8 @@ class matrix_rooms { * * @return string|null */ - public function get_matrix_room_id(): ?string { - if ($this->room_record_exists()) { - return $this->matrixroomrecord->roomid; - } - return null; + public function get_room_id(): ?string { + return $this->record->roomid; } /** @@ -118,19 +138,7 @@ class matrix_rooms { * * @return string|null */ - public function get_matrix_room_topic(): ?string { - if ($this->room_record_exists()) { - return $this->matrixroomrecord->topic; - } - return null; - } - - /** - * Check if room record exist for matrix. - * - * @return bool - */ - public function room_record_exists(): bool { - return (bool) $this->matrixroomrecord; + public function get_topic(): ?string { + return $this->record->topic; } } diff --git a/communication/provider/matrix/classes/privacy/provider.php b/communication/provider/matrix/classes/privacy/provider.php index 06ff1bc8713..7ba1d520aeb 100644 --- a/communication/provider/matrix/classes/privacy/provider.php +++ b/communication/provider/matrix/classes/privacy/provider.php @@ -24,6 +24,7 @@ use core_privacy\local\metadata\null_provider; * @package communication_matrix * @copyright 2023 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore */ class provider implements null_provider { diff --git a/communication/provider/matrix/db/caches.php b/communication/provider/matrix/db/caches.php new file mode 100644 index 00000000000..73e126c9aaa --- /dev/null +++ b/communication/provider/matrix/db/caches.php @@ -0,0 +1,42 @@ +. + +/** + * Cache definition for the Matrix Communication plugin. + * + * @package communication_matrix + * @category cache + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$definitions = [ + // Used to store processed lang files. + // The keys used are the revision, lang and component of the string file. + // The static acceleration size has been based upon student access of the site. + 'serverversions' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => true, + 'staticacceleration' => true, + 'staticaccelerationsize' => 1, + 'canuselocalstore' => true, + // Cache for one day. + 'ttl' => 60 * 60 * 24, + ], +]; diff --git a/communication/provider/matrix/lang/en/communication_matrix.php b/communication/provider/matrix/lang/en/communication_matrix.php index 79fe9aa3c20..e9042297e39 100644 --- a/communication/provider/matrix/lang/en/communication_matrix.php +++ b/communication/provider/matrix/lang/en/communication_matrix.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['cachedef_serverversions'] = 'Matrix server version information for running servers'; $string['matrixuserid'] = 'Matrix user ID'; $string['matrixuserid_desc'] = 'The user ID to be used for Matrix'; $string['matrixhomeserverurl'] = 'Home server URL'; diff --git a/communication/provider/matrix/tests/communication_feature_test.php b/communication/provider/matrix/tests/communication_feature_test.php index a6eb655a84a..b937ccc41f7 100644 --- a/communication/provider/matrix/tests/communication_feature_test.php +++ b/communication/provider/matrix/tests/communication_feature_test.php @@ -17,7 +17,6 @@ namespace communication_matrix; use core_communication\api; -use core_communication\processor; use core_communication\communication_test_helper_trait; use stored_file; @@ -36,7 +35,6 @@ require_once(__DIR__ . '/../../../tests/communication_test_helper_trait.php'); * @coversDefaultClass \communication_matrix\communication_feature */ class communication_feature_test extends \advanced_testcase { - use matrix_test_helper_trait; use communication_test_helper_trait; @@ -51,44 +49,147 @@ class communication_feature_test extends \advanced_testcase { * Test create or update chat room. * * @covers ::create_chat_room + */ + public function test_create_chat_room() { + // Set up the test data first. + $communication = \core_communication\api::load_by_instance( + component: 'communication_matrix', + instancetype: 'example', + instanceid: 1, + ); + + $communication->create_and_configure_room( + selectedcommunication: 'communication_matrix', + communicationroomname: 'Room name', + instance: (object) [ + 'matrixroomtopic' => 'A fun topic', + ], + ); + + /** @var communication_feature */ + $provider = $communication->get_room_provider(); + $this->assertInstanceOf( + communication_feature::class, + $provider, + ); + + // Run the create_chat_room task. + $result = $provider->create_chat_room(); + $this->assertTrue($result); + + // Ensure that a room_id was set. + $this->assertNotEmpty($provider->get_room_id()); + + // Fetch the back office room data. + $remoteroom = $this->backoffice_get_room(); + + // The roomid set in the database must match the one set on the remote server. + $this->assertEquals( + $remoteroom->room_id, + $provider->get_room_id(), + ); + + // The name is a feature of the communication API itself. + $this->assertEquals( + 'Room name', + $communication->get_room_name(), + ); + $this->assertEquals( + $communication->get_room_name(), + $remoteroom->name, + ); + + // The topic is a Matrix feature. + $roomconfig = $provider->get_room_configuration(); + $this->assertEquals( + 'A fun topic', + $roomconfig->get_topic(), + ); + $this->assertEquals( + $remoteroom->topic, + $roomconfig->get_topic(), + ); + + // The avatar features are checked in a separate test. + } + + /** + * Test update of a chat room. + * * @covers ::update_chat_room */ - public function test_create_or_update_chat_room() { - $course = $this->getDataGenerator()->create_course(); - - // Sameple test data. - $instanceid = $course->id; - $component = 'core_course'; - $instancetype = 'coursecommunication'; - $selectedcommunication = 'communication_matrix'; - $communicationroomname = 'communicationroom'; - - $communicationprocessor = processor::create_instance( - $selectedcommunication, - $instanceid, - $component, - $instancetype, - $communicationroomname, + public function test_update_chat_room(): void { + $communication = $this->create_room( + roomname: 'Our room name', + roomtopic: 'Our room topic', ); - $communicationprocessor->get_room_provider()->create_chat_room(); - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); + /** @var communication_feature */ + $provider = $communication->get_room_provider(); + $this->assertInstanceOf( + communication_feature::class, + $provider, + ); - // Test the response against the stored data. - $this->assertNotEmpty($matrixrooms->get_matrix_room_id()); + // Update the room name. + // Note: We have to update the record via the API, and then call the provider update method. + // That's because the update is performed asynchronously. + $communication->update_room( + communicationroomname: 'Our updated room name', + ); + $provider->reload(); - // Add api call to get room data and test against set data. - $matrixroomdata = $this->get_matrix_room_data($matrixrooms->get_matrix_room_id()); - $this->assertEquals($matrixrooms->get_matrix_room_id(), $matrixroomdata->room_id); - $this->assertEquals($communicationprocessor->get_room_name(), $matrixroomdata->name); + // Now call the provider's update method. + $provider->update_chat_room(); - $communicationroomname = 'communicationroomedited'; - $communicationprocessor->update_instance($selectedcommunication, $communicationroomname); - $communicationprocessor->get_room_provider()->update_chat_room(); + // And assert that it was updated remotely. + $remoteroom = $this->backoffice_get_room(); + + $this->assertEquals( + 'Our updated room name', + $communication->get_room_name(), + ); + $this->assertEquals( + $communication->get_room_name(), + $remoteroom->name, + ); + // The remote topic should not have changed. + $this->assertEquals( + 'Our room topic', + $remoteroom->topic, + ); + + // Now update just the topic. + // First in the local API. + $communication->update_room( + instance: (object) [ + 'matrixroomtopic' => 'Our updated room topic', + ], + ); + + // Then call the provider's update method to actually perform the change. + $provider->update_chat_room(); + + // And assert that it was updated remotely. + $remoteroom = $this->backoffice_get_room(); + + $this->assertEquals( + 'Our updated room topic', + $provider->get_room_configuration()->get_topic(), + ); + + // The remote topic should have been updated. + $this->assertEquals( + 'Our updated room topic', + $remoteroom->topic, + ); + + // The name should not have changed. + $this->assertEquals( + 'Our updated room name', + $communication->get_room_name(), + ); - // Add api call to get room data and test against set data. - $matrixroomdata = $this->get_matrix_room_data($matrixrooms->get_matrix_room_id()); - $this->assertEquals($communicationprocessor->get_room_name(), $matrixroomdata->name); } /** @@ -97,37 +198,24 @@ class communication_feature_test extends \advanced_testcase { * @covers ::delete_chat_room */ public function test_delete_chat_room(): void { - $course = $this->getDataGenerator()->create_course(); + $communication = $this->create_room(); - // Sameple test data. - $instanceid = $course->id; - $component = 'core_course'; - $instancetype = 'coursecommunication'; - $selectedcommunication = 'communication_matrix'; - $communicationroomname = 'communicationroom'; + $processor = $communication->get_processor(); + $provider = $communication->get_room_provider(); + $room = matrix_rooms::load_by_processor_id($processor->get_id()); - $communicationprocessor = processor::create_instance( - $selectedcommunication, - $instanceid, - $component, - $instancetype, - $communicationroomname, - ); - $communicationprocessor->get_room_provider()->create_chat_room(); + // Run the delete method. + $this->assertTrue($provider->delete_chat_room()); - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); + // The record of the room should have been removed. + $this->assertNull(matrix_rooms::load_by_processor_id($processor->get_id())); - $communicationprocessor->get_room_provider()->delete_chat_room(); - - // We are not deleting any matrix room, just deleting local record. - $matrixroomsafterdeletion = new matrix_rooms($communicationprocessor->get_id()); - - $this->assertFalse($matrixroomsafterdeletion->room_record_exists()); - - $matrixroomdata = $this->get_matrix_room_data($matrixrooms->get_matrix_room_id()); + // But the room itself shoudl exist. + $matrixroomdata = $this->get_matrix_room_data($room->get_room_id()); $this->assertNotEmpty($matrixroomdata); - $this->assertEquals($communicationprocessor->get_room_name(), $matrixroomdata->name); + $this->assertEquals($processor->get_room_name(), $matrixroomdata->name); + $this->assertEquals($room->get_topic(), $matrixroomdata->topic); } /** @@ -178,16 +266,10 @@ class communication_feature_test extends \advanced_testcase { } // Reload the API instance as the information stored has changed. - $communication = \core_communication\api::load_by_instance( - component: 'communication_matrix', - instancetype: 'example_room', - instanceid: 1, - ); + $communication->reload(); // Update the avatar with the 'after' avatar. $communication->update_room( - 'communication_matrix', - 'Example room name', avatar: $after, ); $this->run_all_adhoc_tasks(); @@ -232,37 +314,19 @@ class communication_feature_test extends \advanced_testcase { * @covers ::get_chat_room_url */ public function test_get_chat_room_url(): void { - $course = $this->get_course('Sampleroom', 'none'); + $communication = $this->create_room(); - // Sample data. - $communicationroomname = 'Sampleroom'; - $selectedcommunication = 'communication_matrix'; + $provider = $communication->get_room_provider(); - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); + $url = $provider->get_chat_room_url(); + $this->assertNotNull($url); - $communication->create_and_configure_room( - $selectedcommunication, - $communicationroomname, - ); + // Fetch the room information from the server. + $remoteroom = $this->backoffice_get_room(); - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - - $communicationprocessor->get_room_provider()->create_chat_room(); - - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - - $this->assertNotNull($communicationprocessor->get_room_provider()->get_chat_room_url()); - $this->assertStringContainsString( - $matrixrooms->get_matrix_room_id(), - $communicationprocessor->get_room_provider()->get_chat_room_url() + $this->assertStringEndsWith( + $remoteroom->room_id, + $url, ); } @@ -273,52 +337,18 @@ class communication_feature_test extends \advanced_testcase { * @covers ::add_registered_matrix_user_to_room */ public function test_create_members(): void { - $course = $this->get_course('Sampleroom', 'none'); - $user = $this->getDataGenerator()->create_user((object) [ - 'username' => 'colin.creavey', - ]); - $userid = $user->id; + $user = $this->getDataGenerator()->create_user(); - // Sample data. - $communicationroomname = 'Sampleroom'; - $selectedcommunication = 'communication_matrix'; - - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id + $communication = $this->create_room( + members: [ + $user->id, + ], ); - $communication->create_and_configure_room( - $selectedcommunication, - $communicationroomname, - ); - $communication->add_members_to_room([$userid]); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - - $communicationprocessor->get_room_provider()->create_chat_room(); - $communicationprocessor->get_room_provider()->add_members_to_room([$userid]); - - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - - // Get created matrixuserid from moodle. - $elementserver = matrix_user_manager::get_formatted_matrix_home_server(); - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - - $this->assertNotNull($matrixuserid); - $this->assertEquals("@{$user->username}:{$elementserver}", $matrixuserid); - - // Add api call to get user data and test against set data. - $matrixuserdata = $this->get_matrix_user_data($matrixrooms->get_matrix_room_id(), $matrixuserid); - - $this->assertNotEmpty($matrixuserdata); - $this->assertEquals(fullname($user), $matrixuserdata->displayname); + $remoteroom = $this->backoffice_get_room(); + $this->assertCount(1, $remoteroom->members); + $member = reset($remoteroom->members); + $this->assertStringStartsWith("@{$user->username}", $member->userid); } /** @@ -330,70 +360,83 @@ class communication_feature_test extends \advanced_testcase { * @covers ::check_room_membership */ public function test_add_and_remove_members_from_room(): void { - $course = $this->get_course('Sampleroom', 'none'); - $userid = $this->get_user()->id; + $user = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); - // Sample data. - $communicationroomname = 'Sampleroom'; - $selectedcommunication = 'communication_matrix'; + $communication = $this->create_room(); + $provider = $communication->get_room_user_provider(); - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); + $remoteroom = $this->backoffice_get_room(); + $this->assertCount(0, $remoteroom->members); - $communication->create_and_configure_room( - $selectedcommunication, - $communicationroomname, - ); - $communication->add_members_to_room([$userid]); + // Add the members to the room. + $provider->add_members_to_room([$user->id, $user2->id]); - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); + // Ensure that they have been created. + $remoteroom = $this->backoffice_get_room(); + $this->assertCount(2, $remoteroom->members); - $communicationprocessor->get_room_provider()->create_chat_room(); - $communicationprocessor->get_room_provider()->add_members_to_room([$userid]); - - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - - // Get created matrixuserid from moodle. - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($userid); - - // Test user is a member of the room. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); + $userids = array_map(fn($member) => $member->userid, $remoteroom->members); + $userids = array_map(fn($userid) => substr($userid, 0, strpos($userid, ':')), $userids); + $this->assertContains("@{$user->username}", $userids); + $this->assertContains("@{$user2->username}", $userids); // Remove member from matrix room. - $communicationprocessor->get_room_provider()->remove_members_from_room([$userid]); + $provider->remove_members_from_room([$user->id]); - // Test user is no longer a member of the room. - $this->assertFalse($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); + // Ensure that they have been removed. + $remoteroom = $this->backoffice_get_room(); + $members = (array) $remoteroom->members; + $this->assertCount(1, $members); + $userids = array_map(fn ($member) => $member->userid, $members); + $userids = array_map(fn ($userid) => substr($userid, 0, strpos($userid, ':')), $userids); + $this->assertNotContains("@{$user->username}", $userids); + $this->assertContains("@{$user2->username}", $userids); } /** - * Test save form data options. + * Helper to create a room. * - * @covers ::save_form_data + * @param null|string $component + * @param null|string $itemtype + * @param null|int $itemid + * @param null|string $roomname + * @param null|string $roomtopic + * @param null|stored_file $roomavatar + * @param array $members + * @return api */ - public function test_save_form_data(): void { - $this->resetAfterTest(); - $course = $this->get_course(); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id + protected function create_room( + ?string $component = 'communication_matrix', + ?string $itemtype = 'example', + ?int $itemid = 1, + ?string $roomname = null, + ?string $roomtopic = null, + ?\stored_file $roomavatar = null, + array $members = [], + ): \core_communication\api { + // Create a new room. + $communication = \core_communication\api::load_by_instance( + component: $component, + instancetype: $itemtype, + instanceid: $itemid, ); - $course->matrixroomtopic = 'Sampletopicupdated'; - $communicationprocessor->get_form_provider()->save_form_data($course); + $communication->create_and_configure_room( + selectedcommunication: 'communication_matrix', + communicationroomname: $roomname ?? 'Room name', + avatar: $roomavatar, + instance: (object) [ + 'matrixroomtopic' => $roomtopic ?? 'A fun topic', + ], + ); - // Test the updated topic. - $matrixroomdata = new matrix_rooms($communicationprocessor->get_id()); - $this->assertEquals('Sampletopicupdated', $matrixroomdata->get_matrix_room_topic()); + $communication->add_members_to_room($members); + + // Run the adhoc task. + $this->run_all_adhoc_tasks(); + + $communication->reload(); + return $communication; } } diff --git a/communication/provider/matrix/tests/fixtures/mocked_matrix_client.php b/communication/provider/matrix/tests/fixtures/mocked_matrix_client.php new file mode 100644 index 00000000000..1ca70c2c9d1 --- /dev/null +++ b/communication/provider/matrix/tests/fixtures/mocked_matrix_client.php @@ -0,0 +1,52 @@ +. + +namespace communication_matrix\tests\fixtures; + +use core\http_client; + +/** + * Tests for the api_base class. + * + * @package communication_matrix + * @category test + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mocked_matrix_client extends \communication_matrix\matrix_client { + /** + * Public variant of the constructor. + */ + public function __construct() { + parent::__construct(...func_get_args()); + } + + /** + * Reset the test client. + */ + public static function reset_client(): void { + self::$client = null; + } + + /** + * Set the http_client to the client specified. + * + * @param http_client $client + */ + public static function set_client(http_client $client): void { + self::$client = $client; + } +} diff --git a/communication/provider/matrix/tests/local/command_test.php b/communication/provider/matrix/tests/local/command_test.php new file mode 100644 index 00000000000..66eac20f75b --- /dev/null +++ b/communication/provider/matrix/tests/local/command_test.php @@ -0,0 +1,412 @@ +. + +namespace communication_matrix\local; + +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; +use ReflectionMethod; + +defined('MOODLE_INTERNAL') || die(); +require_once(dirname(__DIR__) . '/matrix_client_test_trait.php'); + +/** + * Tests for the Matrix command class. + * + * @package communication_matrix + * @category test + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \communication_matrix\local\command + * @coversDefaultClass \communication_matrix\local\command + */ +class command_test extends \advanced_testcase { + use \communication_matrix\matrix_client_test_trait; + + /** + * Test instantiation of a command when no method is provided. + */ + public function test_standard_instantiation(): void { + $instance = $this->get_mocked_instance_for_version('v1.7'); + $command = new command( + $instance, + method: 'PUT', + endpoint: 'example/endpoint', + ); + + // Check the standard functionality. + $this->assertEquals('/example/endpoint', $command->getUri()->getPath()); + $this->assertEquals('PUT', $command->getMethod()); + $this->assertArrayHasKey('Authorization', $command->getHeaders()); + } + + /** + * Test instantiation of a command when no method is provided. + */ + public function test_instantiation_without_auth(): void { + $instance = $this->get_mocked_instance_for_version('v1.7'); + $command = new command( + $instance, + method: 'PUT', + endpoint: 'example/endpoint', + requireauthorization: false, + ); + + // Check the standard functionality. + $this->assertEquals('/example/endpoint', $command->getUri()->getPath()); + $this->assertEquals('PUT', $command->getMethod()); + $this->assertArrayNotHasKey('Authorization', $command->getHeaders()); + } + + /** + * Test processing of command URL properties. + * + * @dataProvider url_parsing_provider + * @param string $url + * @param array $params + * @param string $expected + */ + public function test_url_parsing( + string $url, + array $params, + string $expected, + ): void { + $instance = $this->get_mocked_instance_for_version('v1.7'); + + $command = new command( + $instance, + method: 'PUT', + endpoint: $url, + params: $params, + ); + + $this->assertEquals($expected, $command->getUri()->getPath()); + } + + /** + * Data provider for url parsing tests. + * + * @return array + */ + public function url_parsing_provider(): array { + return [ + [ + 'example/:id/endpoint', + [':id' => '39492'], + '/example/39492/endpoint', + ], + [ + 'example/:id/endpoint/:id', + [':id' => '39492'], + '/example/39492/endpoint/39492', + ], + [ + 'example/:id/endpoint/:id/:name', + [ + ':id' => '39492', + ':name' => 'matrix', + ], + '/example/39492/endpoint/39492/matrix', + ], + ]; + } + + /** + * Test processing of command URL properties with an array which contains untranslated parameters. + */ + public function test_url_parsing_extra_properties(): void { + $instance = $this->get_mocked_instance_for_version('v1.7'); + $this->expectException(\OutOfRangeException::class); + $this->expectExceptionMessage("URL contains untranslated parameters 'example/:id/endpoint'"); + + new command( + $instance, + method: 'PUT', + endpoint: 'example/:id/endpoint', + ); + } + + /** + * Test processing of command URL properties with an array which contains untranslated parameters. + */ + public function test_url_parsing_unused_properites(): void { + $instance = $this->get_mocked_instance_for_version('v1.7'); + $this->expectException(\OutOfRangeException::class); + $this->expectExceptionMessage("Parameter not found in URL ':id'"); + + new command( + $instance, + method: 'PUT', + endpoint: 'example/:ids/endpoint', + params: [ + ':id' => 12345, + ], + ); + } + + /** + * Test the parameter fetching, processing, and parsing. + * + * @dataProvider parameter_and_option_provider + * @param string $endpoint + * @param array $params + * @param array $remainingparams + * @param array $allparams + * @param array $options + */ + public function test_parameters( + string $endpoint, + array $params, + array $remainingparams, + array $allparams, + array $options, + ): void { + $instance = $this->get_mocked_instance_for_version('v1.7'); + + $command = new command( + $instance, + method: 'PUT', + endpoint: $endpoint, + params: $params, + ); + + $this->assertSame($remainingparams, $command->get_remaining_params()); + $this->assertSame($allparams, $command->get_all_params()); + $this->assertSame($options, $command->get_options()); + } + + /** + * Data provider for parameter tests. + * + * @return array + */ + public function parameter_and_option_provider(): array { + $command = [ + 'method' => 'PUT', + 'endpoint' => 'example/:id/endpoint', + ]; + + return [ + 'no parameters' => [ + 'endpoint' => 'example/endpoint', + 'params' => [], + 'remainingparams' => [], + 'allparams' => [], + 'options' => [ + 'json' => [], + ], + ], + 'named params' => [ + 'endpoint' => 'example/:id/endpoint', + 'params' => [ + ':id' => 12345, + ], + 'remainingparams' => [], + 'allparams' => [ + ':id' => 12345, + ], + 'options' => [ + 'json' => [], + ], + ], + 'mixture of params' => [ + 'endpoint' => 'example/:id/endpoint', + 'params' => [ + ':id' => 12345, + 'name' => 'matrix', + ], + 'remainingparams' => [ + 'name' => 'matrix', + ], + 'allparams' => [ + ':id' => 12345, + 'name' => 'matrix', + ], + 'options' => [ + 'json' => [ + 'name' => 'matrix', + ], + ], + ], + ]; + } + + /** + * Test the query parameter handling. + * + * @dataProvider query_provider + * @param array $query + * @param string $expected + */ + public function test_query_parameters( + array $query, + string $expected, + ): void { + // The query parameter is only added at the time we call send. + // That's because it can only be provided to Guzzle as an Option, not as part of the URL. + // Options can only be applied at time of transfer. + // Unfortuantely that leads to slightly less ideal testing that we'd like here. + $mock = new MockHandler(); + $instance = $this->get_mocked_instance_for_version( + 'v1.7', + mock: $mock, + ); + + $mock->append(function(Request $request) use ($expected): Response { + $this->assertSame( + $expected, + $request->getUri()->getQuery(), + ); + return new Response(); + }); + $command = new command( + $instance, + method: 'PUT', + endpoint: 'example/endpoint', + query: $query, + ); + + $execute = new ReflectionMethod($instance, 'execute'); + $execute->setAccessible(true); + $execute->invoke($instance, $command); + } + + /** + * Data provider for query parameter tests. + * @return array + */ + public function query_provider(): array { + return [ + 'no query' => [ + 'query' => [], + 'expected' => '', + ], + 'single query' => [ + 'query' => [ + 'name' => 'matrix', + ], + 'expected' => 'name=matrix', + ], + 'multiple queries' => [ + 'query' => [ + 'name' => 'matrix', + 'type' => 'room', + ], + 'expected' => 'name=matrix&type=room', + ], + ]; + } + + /** + * Test the sendasjson constructor parameter. + * + * @dataProvider sendasjson_provider + * @param bool $sendasjson + * @param string $endpoint + * @param array $params + * @param array $remainingparams + * @param array $allparams + * @param array $expectedoptions + */ + public function test_send_as_json( + bool $sendasjson, + string $endpoint, + array $params, + array $remainingparams, + array $allparams, + array $expectedoptions, + ): void { + $instance = $this->get_mocked_instance_for_version('v1.7'); + + $command = new command( + $instance, + method: 'PUT', + endpoint: $endpoint, + params: $params, + sendasjson: $sendasjson, + ); + + $this->assertSame($remainingparams, $command->get_remaining_params()); + $this->assertSame($allparams, $command->get_all_params()); + $this->assertSame($expectedoptions, $command->get_options()); + } + + /** + * Test the sendasjosn option to the command constructor. + * + * @return array + */ + public function sendasjson_provider(): array { + return [ + 'As JSON' => [ + 'sendasjon' => true, + 'endpoint' => 'example/:id/endpoint', + 'params' => [ + ':id' => 12345, + 'name' => 'matrix', + ], + 'remainingparams' => [ + 'name' => 'matrix', + ], + 'allparams' => [ + ':id' => 12345, + 'name' => 'matrix', + ], + 'expectedoptions' => [ + 'json' => [ + 'name' => 'matrix', + ], + ], + ], + 'Not as JSON' => [ + 'sendasjson' => false, + 'endpoint' => 'example/:id/endpoint', + 'params' => [ + ':id' => 12345, + 'name' => 'matrix', + ], + 'remainingparams' => [ + 'name' => 'matrix', + ], + 'allparams' => [ + ':id' => 12345, + 'name' => 'matrix', + ], + 'expectedoptions' => [ + ], + ], + ]; + } + + /** + * Test the sendasjosn option to the command constructor. + */ + public function test_ignorehttperrors(): void { + $instance = $this->get_mocked_instance_for_version('v1.7'); + + $command = new command( + $instance, + method: 'PUT', + endpoint: 'example/endpoint', + ignorehttperrors: true, + ); + + $options = $command->get_options(); + $this->assertArrayHasKey('http_errors', $options); + $this->assertFalse($options['http_errors']); + } +} diff --git a/communication/provider/matrix/tests/matrix_client_test.php b/communication/provider/matrix/tests/matrix_client_test.php new file mode 100644 index 00000000000..272bea4e327 --- /dev/null +++ b/communication/provider/matrix/tests/matrix_client_test.php @@ -0,0 +1,413 @@ +. + +namespace communication_matrix; + +use communication_matrix\local\command; +use communication_matrix\local\spec\v1p7; +use communication_matrix\local\spec\features; +use communication_matrix\tests\fixtures\mocked_matrix_client; +use core\http_client; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use GuzzleHttp\Psr7\Response; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); +require_once(__DIR__ . '/matrix_client_test_trait.php'); + +/** + * Tests for the matrix_client class. + * + * @package communication_matrix + * @category test + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \communication_matrix\matrix_client + * @coversDefaultClass \communication_matrix\matrix_client + */ +class matrix_client_test extends \advanced_testcase { + use matrix_client_test_trait; + + /** + * Data provider for valid calls to ::instance. + * @return array + */ + public function instance_provider(): array { + $testcases = [ + 'Standard versions' => [ + null, + v1p7::class, + ], + ]; + + // Remove a couple of versions. + $versions = $this->get_current_versions(); + array_pop($versions); + array_pop($versions); + + $testcases['Older server'] = [ + $versions, + array_key_last($versions), + ]; + + // Limited version compatibility, including newer than we support now. + $testcases['Newer versions with crossover'] = [ + [ + 'v1.6', + 'v1.7', + 'v7.9', + ], + \communication_matrix\local\spec\v1p7::class, + ]; + + return $testcases; + } + + /** + * Test that the instance method returns a valid instance for the given versions. + * + * @dataProvider instance_provider + * @param array|null $versions + * @param string $expectedversion + */ + public function test_instance( + ?array $versions, + string $expectedversion, + ): void { + // Create a mock and queue two responses. + + $mock = new MockHandler([ + $this->get_mocked_version_response($versions), + ]); + $handlerstack = HandlerStack::create($mock); + $container = []; + $history = Middleware::history($container); + $handlerstack->push($history); + $client = new http_client(['handler' => $handlerstack]); + mocked_matrix_client::set_client($client); + + $instance = mocked_matrix_client::instance( + 'https://example.com', + 'testtoken', + ); + + $this->assertInstanceOf(matrix_client::class, $instance); + + // Only the version API has been called. + $this->assertCount(1, $container); + $request = reset($container); + $this->assertEquals('/_matrix/client/versions', $request['request']->getUri()->getPath()); + + // The client should be a v1p7 client as that is the highest compatible version. + $this->assertInstanceOf($expectedversion, $instance); + } + + /** + * Test that the instance method returns a valid instance for the given versions. + */ + public function test_instance_cached(): void { + $mock = new MockHandler([ + $this->get_mocked_version_response(), + $this->get_mocked_version_response(), + ]); + $handlerstack = HandlerStack::create($mock); + $container = []; + $history = Middleware::history($container); + $handlerstack->push($history); + $client = new http_client(['handler' => $handlerstack]); + mocked_matrix_client::set_client($client); + + $instance = mocked_matrix_client::instance('https://example.com', 'testtoken'); + + $this->assertInstanceOf(matrix_client::class, $instance); + + // Only the version API has been called. + $this->assertCount(1, $container); + + // Call the API again. It should not lead to additional fetches. + $instance = mocked_matrix_client::instance('https://example.com', 'testtoken'); + $instance = mocked_matrix_client::instance('https://example.com', 'testtoken'); + $this->assertCount(1, $container); + + // But a different endpoint will. + $instance = mocked_matrix_client::instance('https://example.org', 'testtoken'); + $this->assertCount(2, $container); + } + + /** + * Test that the instance method throws an appropriate exception if no support is found. + */ + public function test_instance_no_support(): void { + // Create a mock and queue two responses. + + $mock = new MockHandler([ + $this->get_mocked_version_response(['v99.9']), + ]); + $handlerstack = HandlerStack::create($mock); + $container = []; + $history = Middleware::history($container); + $handlerstack->push($history); + $client = new http_client(['handler' => $handlerstack]); + mocked_matrix_client::set_client($client); + + $this->expectException(moodle_exception::class); + $this->expectExceptionMessage('No supported Matrix API versions found.'); + + mocked_matrix_client::instance( + 'https://example.com', + 'testtoken', + ); + } + + /** + * Test the feature implementation check methods. + * + * @covers ::implements_feature + * @covers ::get_supported_versions + * @dataProvider implements_feature_provider + * @param string $version + * @param array|string $features + * @param bool $expected + */ + public function test_implements_feature( + string $version, + array|string $features, + bool $expected, + ): void { + $instance = $this->get_mocked_instance_for_version($version); + $this->assertEquals($expected, $instance->implements_feature($features)); + } + + /** + * Test the feature implementation requirement methods. + * + * @covers ::implements_feature + * @covers ::get_supported_versions + * @covers ::require_feature + * @dataProvider implements_feature_provider + * @param string $version + * @param array|string $features + * @param bool $expected + */ + public function test_require_feature( + string $version, + array|string $features, + bool $expected, + ): void { + $instance = $this->get_mocked_instance_for_version($version); + + if ($expected) { + $this->assertEmpty($instance->require_feature($features)); + } else { + $this->expectException('moodle_exception'); + $instance->require_feature($features); + } + } + + /** + * Test the feature implementation requirement methods for a require all. + * + * @covers ::implements_feature + * @covers ::get_supported_versions + * @covers ::require_feature + * @covers ::require_features + * @dataProvider require_features_provider + * @param string $version + * @param array|string $features + * @param bool $expected + */ + public function test_require_features( + string $version, + array|string $features, + bool $expected, + ): void { + $instance = $this->get_mocked_instance_for_version($version); + + if ($expected) { + $this->assertEmpty($instance->require_features($features)); + } else { + $this->expectException('moodle_exception'); + $instance->require_features($features); + } + } + + /** + * Data provider for feature implementation check tests. + * + * @return array + */ + public function implements_feature_provider(): array { + return [ + '[supported] as array' => [ + 'v1.6', + [features\matrix\create_room_v3::class], + true, + ], + '[supported, supported] as array' => [ + 'v1.6', + [ + features\matrix\create_room_v3::class, + features\matrix\update_room_avatar_v3::class, + ], + true, + ], + ]; + } + + /** + * Data provider for feature implementation check tests. + * + * @return array + */ + public function require_features_provider(): array { + // We'll just add to the standard testcases. + $testcases = array_map(static function(array $testcase): array { + $testcase[1] = [$testcase[1]]; + return $testcase; + }, $this->implements_feature_provider()); + + $testcases['Require many supported features'] = [ + 'v1.6', + [ + features\matrix\create_room_v3::class, + features\matrix\update_room_avatar_v3::class, + ], + true, + ]; + + return $testcases; + } + + /** + * Test the get_version method. + * + * @param string $version + * @param string $expectedversion + * @dataProvider get_version_provider + * @covers ::get_version + * @covers ::get_version_from_classname + */ + public function test_get_version( + string $version, + string $expectedversion, + ): void { + $instance = $this->get_mocked_instance_for_version($version); + $this->assertEquals($expectedversion, $instance->get_version()); + } + + /** + * Data provider for get_version tests. + * + * @return array + */ + public function get_version_provider(): array { + return [ + ['v1.1', '1.1'], + ['v1.7', '1.7'], + ]; + } + + /** + * Tests the meets_version method. + * + * @param string $version The version of the API to test against + * @param string $testversion The version to test + * @param bool $expected Whether the version meets the requirement + * @dataProvider meets_version_provider + * @covers ::meets_version + */ + public function test_meets_version( + string $version, + string $testversion, + bool $expected, + ): void { + $instance = $this->get_mocked_instance_for_version($version); + $this->assertEquals($expected, $instance->meets_version($testversion)); + } + + /** + * Tests the requires_version method. + * + * @param string $version The version of the API to test against + * @param string $testversion The version to test + * @param bool $expected Whether the version meets the requirement + * @dataProvider meets_version_provider + * @covers ::requires_version + */ + public function test_requires_version( + string $version, + string $testversion, + bool $expected, + ): void { + $instance = $this->get_mocked_instance_for_version($version); + + if ($expected) { + $this->assertEmpty($instance->requires_version($testversion)); + } else { + $this->expectException('moodle_exception'); + $instance->requires_version($testversion); + } + } + + /** + * Data provider for meets_version tests. + * + * @return array + */ + public function meets_version_provider(): array { + return [ + 'Same version' => ['v1.1', '1.1', true], + 'Same version latest' => ['v1.7', '1.7', true], + 'Newer version rejected' => ['v1.1', '1.7', false], + 'Older version accepted' => ['v1.7', '1.1', true], + ]; + } + + /** + * Test the execute method with a command. + * + * @covers ::execute + */ + public function test_command_is_executed(): void { + $historycontainer = []; + $mock = new MockHandler(); + + $instance = $this->get_mocked_instance_for_version('v1.6', $historycontainer, $mock); + $command = new command( + $instance, + method: 'GET', + endpoint: 'test/endpoint', + params: [ + 'test' => 'test', + ], + ); + + $mock->append(new Response(200)); + + $rc = new \ReflectionClass($instance); + $rcm = $rc->getMethod('execute'); + $rcm->setAccessible(true); + $result = $rcm->invoke($instance, $command); + + $this->assertEquals(200, $result->getStatusCode()); + $this->assertCount(1, $historycontainer); + $request = array_shift($historycontainer); + $this->assertEquals('GET', $request['request']->getMethod()); + $this->assertEquals('/test/endpoint', $request['request']->getUri()->getPath()); + } +} diff --git a/communication/provider/matrix/tests/matrix_client_test_trait.php b/communication/provider/matrix/tests/matrix_client_test_trait.php new file mode 100644 index 00000000000..72bda517c54 --- /dev/null +++ b/communication/provider/matrix/tests/matrix_client_test_trait.php @@ -0,0 +1,169 @@ +. + +namespace communication_matrix; + +use communication_matrix\local\spec\{v1p1, v1p2, v1p3, v1p4, v1p5, v1p6, v1p7}; +use communication_matrix\tests\fixtures\mocked_matrix_client; +use core\http_client; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use GuzzleHttp\Psr7\Response; + +/** + * A trait with shared tooling for handling matrix_client tests. + * + * @package communication_matrix + * @category test + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait matrix_client_test_trait { + public static function setUpBeforeClass(): void { + parent::setUpBeforeClass(); + + // Ensure that the mocked client is available. + require_once(__DIR__ . '/fixtures/mocked_matrix_client.php'); + } + + public function setUp(): void { + parent::setUp(); + + // Reset the test client. + mocked_matrix_client::reset_client(); + } + + public function tearDown():void { + parent::tearDown(); + + // Reset the test client. + mocked_matrix_client::reset_client(); + } + + /** + * Get a mocked instance for a specific Matrix API version, + * + * @param string $version + * @param array $historycontainer An array which will be filled with history for the mocked client. + * @param MockHandler|null $mock A MockHandler object that can be appended to + * @return matrix_client + */ + protected function get_mocked_instance_for_version( + string $version, + array &$historycontainer = [], + ?MockHandler $mock = null, + ): matrix_client { + if ($mock === null) { + $mock = new MockHandler(); + } + // Add the version response. + $mock->append($this->get_mocked_version_response([$version])); + + $handlerstack = HandlerStack::create($mock); + $history = Middleware::history($historycontainer); + $handlerstack->push($history); + $client = new http_client(['handler' => $handlerstack]); + mocked_matrix_client::set_client($client); + + $client = mocked_matrix_client::instance( + 'https://example.com', + 'testtoken', + ); + + // Remove the request that is required to fetch the version from the history. + array_shift($historycontainer); + + return $client; + } + + /** + * Get a mocked response for the /versions well-known URI. + * + * @param array|null $versions + * @param array|null $unstablefeatures + * @return Response + */ + protected function get_mocked_version_response( + array $versions = null, + array $unstablefeatures = null, + ): Response { + $data = (object) [ + "versions" => array_values($this->get_current_versions()), + "unstable_features" => $this->get_current_unstable_features(), + ]; + + if ($versions) { + $data->versions = array_values($versions); + } + + if ($unstablefeatures) { + $data->unstable_features = $unstablefeatures; + } + + return new Response(200, [], json_encode($data)); + } + + /** + * A helper to get the current versions returned by synapse. + * + * @return array + */ + protected function get_current_versions(): array { + return [ + v1p1::class => "v1.1", + v1p2::class => "v1.2", + v1p3::class => "v1.3", + v1p4::class => "v1.4", + v1p5::class => "v1.5", + v1p6::class => "v1.6", + v1p7::class => "v1.7", + ]; + } + + /** + * A helper to get the current unstable features returned by synapse. + * @return array + */ + protected function get_current_unstable_features(): array { + return [ + "org.matrix.label_based_filtering" => true, + "org.matrix.e2e_cross_signing" => true, + "org.matrix.msc2432" => true, + "uk.half-shot.msc2666.query_mutual_rooms" => true, + "io.element.e2ee_forced.public" => false, + "io.element.e2ee_forced.private" => false, + "io.element.e2ee_forced.trusted_private" => false, + "org.matrix.msc3026.busy_presence" => false, + "org.matrix.msc2285.stable" => true, + "org.matrix.msc3827.stable" => true, + "org.matrix.msc2716" => false, + "org.matrix.msc3440.stable" => true, + "org.matrix.msc3771" => true, + "org.matrix.msc3773" => false, + "fi.mau.msc2815" => false, + "fi.mau.msc2659.stable" => true, + "org.matrix.msc3882" => false, + "org.matrix.msc3881" => false, + "org.matrix.msc3874" => false, + "org.matrix.msc3886" => false, + "org.matrix.msc3912" => false, + "org.matrix.msc3952_intentional_mentions" => false, + "org.matrix.msc3981" => false, + "org.matrix.msc3391" => false, + ]; + } +} diff --git a/communication/provider/matrix/tests/matrix_communication_test.php b/communication/provider/matrix/tests/matrix_communication_test.php deleted file mode 100644 index 5191b70b336..00000000000 --- a/communication/provider/matrix/tests/matrix_communication_test.php +++ /dev/null @@ -1,956 +0,0 @@ -. - -namespace communication_matrix; - -use core_communication\processor; -use core_communication\communication_test_helper_trait; - -defined('MOODLE_INTERNAL') || die(); - -require_once(__DIR__ . '/matrix_test_helper_trait.php'); -require_once(__DIR__ . '/../../../tests/communication_test_helper_trait.php'); - -/** - * Class matrix_provider_test to test the matrix provider scenarios using the matrix endpoints. - * - * @package communication_matrix - * @category test - * @copyright 2023 Safat Shahin - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class matrix_communication_test extends \advanced_testcase { - - use matrix_test_helper_trait; - use communication_test_helper_trait; - - public function setUp(): void { - parent::setUp(); - $this->resetAfterTest(); - $this->setup_communication_configs(); - $this->initialise_mock_server(); - } - - /** - * Test creating course with matrix provider creates all the associated data and matrix room. - * - * @covers \core_communication\api::create_and_configure_room - * @covers \core_communication\task\create_and_configure_room_task::execute - * @covers \core_communication\task\create_and_configure_room_task::queue - */ - public function test_create_course_with_matrix_provider(): void { - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - - // Run the task. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id, - ); - - // Initialize the matrix room object. - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - - // Test against the data. - $matrixroomdata = $this->get_matrix_room_data($matrixrooms->get_matrix_room_id()); - $this->assertEquals($matrixrooms->get_matrix_room_id(), $matrixroomdata->room_id); - $this->assertEquals($roomname, $matrixroomdata->name); - } - - /** - * Test update course with matrix provider. - * - * @covers \core_communication\api::update_room - * @covers \core_communication\task\update_room_task::execute - * @covers \core_communication\task\update_room_task::queue - */ - public function test_update_course_with_matrix_provider(): void { - global $CFG; - $course = $this->get_course(); - - // Run the task. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Sample data. - $communicationroomname = 'Sampleroomupdated'; - $selectedcommunication = 'communication_matrix'; - $logo = $this->create_communication_file('moodle_logo.jpg', 'logo.jpg'); - - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id, - ); - $communication->update_room($selectedcommunication, $communicationroomname, $logo); - - // Pending avatar update should indicate avatar is not in sync. - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $this->assertFalse($communicationprocessor->is_avatar_synced()); - - // Run the task. - $this->runAdhocTasks('\core_communication\task\update_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - - // Check that the avatar is now synced with Matrix again. - $this->assertTrue($communicationprocessor->is_avatar_synced()); - - // Initialize the matrix room object. - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - - // Test against the data. - $matrixroomdata = $this->get_matrix_room_data($matrixrooms->get_matrix_room_id()); - $this->assertEquals($matrixrooms->get_matrix_room_id(), $matrixroomdata->room_id); - $this->assertEquals($communicationroomname, $matrixroomdata->name); - } - - /** - * Test course delete with matrix provider. - * - * @covers \core_communication\api::delete_room - * @covers \core_communication\task\delete_room_task::execute - * @covers \core_communication\task\delete_room_task::queue - */ - public function test_delete_course_with_matrix_provider(): void { - global $DB; - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - - // Run the task. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $communicationid = $communicationprocessor->get_id(); - - // Initialize the matrix room object. - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - - // Test against the data. - $matrixroomdata = $this->get_matrix_room_data($matrixrooms->get_matrix_room_id()); - $this->assertEquals($matrixrooms->get_matrix_room_id(), $matrixroomdata->room_id); - - // Now delete the course. - delete_course($course, false); - - // Run the task. - $this->runAdhocTasks('\core_communication\task\delete_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $this->assertNull($communicationprocessor); - - // Initialize the matrix room object. - $matrixrooms = $DB->get_record('matrix_rooms', ['commid' => $communicationid]); - $this->assertEmpty($matrixrooms); - } - - /** - * Test creating course with matrix provider creates all the associated data and matrix room. - * - * @covers \core_communication\api::add_members_to_room - * @covers \core_communication\task\add_members_to_room_task::execute - * @covers \core_communication\task\add_members_to_room_task::queue - */ - public function test_create_members_with_matrix_provider(): void { - $course = $this->get_course('Samplematrixroom', 'communication_matrix'); - $user = $this->get_user('Samplefnmatrix', 'Samplelnmatrix', 'sampleunmatrix'); - - // Run room operation task. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Enrol the user in the course. - $enrol = enrol_get_plugin('manual'); - $enrolinstances = enrol_get_instances($course->id, true); - $enrol->enrol_user(reset($enrolinstances), $user->id); - - // Run user operation task. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - - // Get matrix user id from moodle. - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - $this->assertNotNull($matrixuserid); - - // Get matrix user id from matrix. - $matrixuserdata = $this->get_matrix_user_data($matrixrooms->get_matrix_room_id(), $matrixuserid); - $this->assertNotEmpty($matrixuserdata); - $this->assertEquals("Samplefnmatrix Samplelnmatrix", $matrixuserdata->displayname); - } - - /** - * Test enrolment adds the user to a Matrix room. - * - * @covers \core_communication\api::add_members_to_room - * @covers \core_communication\task\add_members_to_room_task::execute - * @covers \core_communication\task\add_members_to_room_task::queue - */ - public function test_enrolling_user_adds_user_to_matrix_room(): void { - global $CFG; - require_once($CFG->dirroot . '/lib/enrollib.php'); - - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - $user = $this->get_user(); - - // Run room tasks. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Enrol the user in the course. - $enrol = enrol_get_plugin('manual'); - $enrolinstances = enrol_get_instances($course->id, true); - $instance = reset($enrolinstances); - $enrol->enrol_user($instance, $user->id); - - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - $matrixhomeserverurl = $eventmanager->matrixhomeserverurl; - - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - // Check our Matrix user id has room membership. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - } - - /** - * Test enrolment removes the user from a Matrix room. - * - * @covers \core_communication\api::remove_members_from_room - * @covers \core_communication\task\remove_members_from_room::execute - * @covers \core_communication\task\remove_members_from_room::queue - */ - public function test_unenrolling_user_removes_user_from_matrix_room(): void { - global $CFG; - require_once($CFG->dirroot . '/lib/enrollib.php'); - - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - $user = $this->get_user(); - - // Run room tasks. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Enrol the user in the course. - $enrol = enrol_get_plugin('manual'); - $enrolinstances = enrol_get_instances($course->id, true); - $instance = reset($enrolinstances); - $enrol->enrol_user($instance, $user->id); - - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - $matrixhomeserverurl = $eventmanager->matrixhomeserverurl; - - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - // Check our Matrix user id has room membership. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - // Unenrol the user from the course. - $enrol->unenrol_user($instance, $user->id); - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\remove_members_from_room'); - // Check our Matrix user id no longer has membership. - $this->assertFalse($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - } - - /** - * Test enrolled users in a course lose access to a room when their enrolment is suspended. - * - * @covers \core_communication\api::remove_members_from_room - * @covers \core_communication\task\remove_members_from_room::execute - * @covers \core_communication\task\remove_members_from_room::queue - */ - public function test_users_removed_from_room_when_suspending_enrolment(): void { - global $CFG; - require_once($CFG->dirroot . '/lib/enrollib.php'); - - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - $user = $this->get_user(); - - // Run room tasks. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Enrol the user in the course. - $enrol = enrol_get_plugin('manual'); - $enrolinstances = enrol_get_instances($course->id, true); - $instance = reset($enrolinstances); - $enrol->enrol_user($instance, $user->id); - - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - $matrixhomeserverurl = $eventmanager->matrixhomeserverurl; - - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - // Check our Matrix user id has room membership. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - // Suspend user enrolment. - $enrol->update_user_enrol($instance, $user->id, 1); - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\remove_members_from_room'); - // Check our Matrix user id no longer has membership. - $this->assertFalse($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - } - - /** - * Test enrolled users in a course lose access to a room when the instance is deleted. - * - * @covers \core_communication\api::remove_members_from_room - * @covers \core_communication\task\remove_members_from_room::execute - * @covers \core_communication\task\remove_members_from_room::queue - */ - public function test_users_removed_from_room_when_deleting_instance(): void { - global $CFG; - require_once($CFG->dirroot . '/lib/enrollib.php'); - - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - $user = $this->get_user(); - - // Run room tasks. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Enrol the user in the course. - $enrol = enrol_get_plugin('manual'); - $enrolinstances = enrol_get_instances($course->id, true); - $instance = reset($enrolinstances); - $enrol->enrol_user($instance, $user->id); - - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - $matrixhomeserverurl = $eventmanager->matrixhomeserverurl; - - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - // Check our Matrix user id has room membership. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - // Delete instance. - $enrol->delete_instance($instance); - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\remove_members_from_room'); - // Check our Matrix user id no longer has membership. - $this->assertFalse($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - } - - /** - * Test enrolled users in a course lose access to a room when the instance is disabled. - * - * @covers \core_communication\api::remove_members_from_room - * @covers \core_communication\task\remove_members_from_room::execute - * @covers \core_communication\task\remove_members_from_room::queue - */ - public function test_users_removed_from_room_when_disabling_instance(): void { - global $CFG; - require_once($CFG->dirroot . '/lib/enrollib.php'); - - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - $user = $this->get_user(); - - // Run room tasks. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Enrol the user in the course. - $enrol = enrol_get_plugin('manual'); - $enrolinstances = enrol_get_instances($course->id, true); - $instance = reset($enrolinstances); - $enrol->enrol_user($instance, $user->id); - - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - $matrixhomeserverurl = $eventmanager->matrixhomeserverurl; - - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - // Check our Matrix user id has room membership. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - // Update enrolment communication. - $enrol->update_communication($instance->id, 'remove', $course->id); - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\remove_members_from_room'); - // Check our Matrix user id no longer has membership. - $this->assertFalse($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - } - - /** - * Test enrolled users memerbship toggles correctly when an instance is disabled and reenabled again. - * - * @covers \core_communication\api::add_members_to_room - * @covers \core_communication\task\add_members_to_room_task::execute - * @covers \core_communication\task\add_members_to_room_task::queue - * @covers \core_communication\api::remove_members_from_room - * @covers \core_communication\task\remove_members_from_room::execute - * @covers \core_communication\task\remove_members_from_room::queue - */ - public function test_users_memerbship_toggles_when_disabling_and_reenabling_instance(): void { - global $CFG; - require_once($CFG->dirroot . '/lib/enrollib.php'); - - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - $user = $this->get_user(); - - // Run room tasks. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Enrol the user in the course. - $enrol = enrol_get_plugin('manual'); - $enrolinstances = enrol_get_instances($course->id, true); - $instance = reset($enrolinstances); - $enrol->enrol_user($instance, $user->id); - - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - $matrixhomeserverurl = $eventmanager->matrixhomeserverurl; - - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - // Check our Matrix user id has room membership. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - // Update enrolment communication when updating instance to disabled. - $enrol->update_communication($instance->id, 'remove', $course->id); - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\remove_members_from_room'); - // Check our Matrix user id no longer has membership. - $this->assertFalse($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - // Update enrolment communication when updating instance to enabled. - $enrol->update_communication($instance->id, 'add', $course->id); - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - // Check our Matrix user id no longer has membership. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - } - - /** - * Test enrolled users in a course lose access to a room when the provider is disabled. - * - * @covers \core_communication\api::remove_members_from_room - * @covers \core_communication\task\remove_members_from_room::execute - * @covers \core_communication\task\remove_members_from_room::queue - */ - public function test_users_removed_from_room_when_disabling_provider(): void { - global $CFG; - require_once($CFG->dirroot . '/lib/enrollib.php'); - require_once($CFG->dirroot . '/course/lib.php'); - - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - $user = $this->get_user(); - - // Run room tasks. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Enrol the user in the course. - $enrol = enrol_get_plugin('manual'); - $enrolinstances = enrol_get_instances($course->id, true); - $instance = reset($enrolinstances); - $enrol->enrol_user($instance, $user->id); - - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - $matrixhomeserverurl = $eventmanager->matrixhomeserverurl; - - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - // Check our Matrix user id has room membership. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - // Disable communication provider. - $course->selectedcommunication = 'none'; - update_course($course); - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\remove_members_from_room'); - // Check our Matrix user id no longer has membership. - $this->assertFalse($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - } - - /** - * Test enrolled users in a course lose access to a room when their user account is suspended. - * - * @covers \core_communication\api::remove_members_from_room - * @covers \core_communication\task\remove_members_from_room::execute - * @covers \core_communication\task\remove_members_from_room::queue - */ - public function test_users_removed_from_room_when_suspending_user(): void { - global $CFG; - require_once($CFG->dirroot . '/lib/enrollib.php'); - - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - $user = $this->get_user(); - - // Run room tasks. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Enrol the user in the course. - $enrol = enrol_get_plugin('manual'); - $enrolinstances = enrol_get_instances($course->id, true); - $instance = reset($enrolinstances); - $enrol->enrol_user($instance, $user->id); - - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - $matrixhomeserverurl = $eventmanager->matrixhomeserverurl; - - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - // Check our Matrix user id has room membership. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - // Suspend user. - $user->suspended = 1; - user_update_user($user, false, false); - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\remove_members_from_room'); - // Check our Matrix user id no longer has membership. - $this->assertFalse($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - } - - /** - * Test enrolled users in a course lose access to a room when their user account is deleted. - * - * @covers \core_communication\api::remove_members_from_room - * @covers \core_communication\task\remove_members_from_room::execute - * @covers \core_communication\task\remove_members_from_room::queue - */ - public function test_users_removed_from_room_when_deleting_user(): void { - global $CFG; - require_once($CFG->dirroot . '/lib/enrollib.php'); - - // Sample data. - $roomname = 'Samplematrixroom'; - $provider = 'communication_matrix'; - $course = $this->get_course($roomname, $provider); - $user = $this->get_user(); - - // Run room tasks. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Enrol the user in the course. - $enrol = enrol_get_plugin('manual'); - $enrolinstances = enrol_get_instances($course->id, true); - $instance = reset($enrolinstances); - $enrol->enrol_user($instance, $user->id); - - // Run the user tasks. - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $matrixrooms = new matrix_rooms($communicationprocessor->get_id()); - $eventmanager = new matrix_events_manager($matrixrooms->get_matrix_room_id()); - $matrixhomeserverurl = $eventmanager->matrixhomeserverurl; - - $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($user->id); - // Check our Matrix user id has room membership. - $this->assertTrue($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - // Delete user. - delete_user($user); - // Run the user tasks. - // $this->runAdhocTasks('\core_communication\task\remove_members_from_room'); - // Check our Matrix user id no longer has membership. - $this->assertFalse($communicationprocessor->get_room_provider()->check_room_membership($matrixuserid)); - } - - /** - * Test create instance user mapping. - * - * @covers \core_communication\processor::create_instance_user_mapping - * @covers \core_communication\processor::mark_users_as_synced - * @covers \core_communication\processor::get_instance_userids - */ - public function test_create_instance_user_mapping(): void { - $this->resetAfterTest(); - - global $DB; - $course = $this->get_course('Sampleroom', 'none'); - $userid = $this->get_user()->id; - - // Sample data. - $communicationroomname = 'Sampleroom'; - $selectedcommunication = 'communication_matrix'; - $component = 'core_course'; - $instancetype = 'coursecommunication'; - - // First test the adding members to a room. - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $communication->create_and_configure_room($selectedcommunication, $communicationroomname); - $communication->add_members_to_room([$userid]); - - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - // Test against the object. - $communicationprocessor = processor::load_by_instance( - $component, - $instancetype, - $course->id - ); - - // Test against the database. - $communicationuserrecord = $DB->get_record('communication_user', [ - 'commid' => $communicationprocessor->get_id(), - 'userid' => $userid - ]); - - $this->assertEquals($communicationuserrecord->userid, $userid); - $this->assertEquals($communicationuserrecord->commid, $communicationprocessor->get_id()); - } - - /** - * Test update instance user mapping. - * - * @covers \core_communication\processor::create_instance_user_mapping - * @covers \core_communication\processor::mark_users_as_synced - * @covers \core_communication\processor::get_instance_userids - * @covers \core_communication\processor::delete_instance_user_mapping - */ - public function test_update_instance_user_mapping(): void { - $this->resetAfterTest(); - - global $DB; - $course = $this->get_course(); - $userid = $this->get_user()->id; - - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Sample data. - $communicationroomname = 'Sampleroom'; - $selectedcommunication = 'communication_matrix'; - $component = 'core_course'; - $instancetype = 'coursecommunication'; - - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $communication->update_room($selectedcommunication, $communicationroomname); - $communication->add_members_to_room([$userid]); - - $this->runAdhocTasks('\core_communication\task\update_room_task'); - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - // Test against the object. - $communicationprocessor = processor::load_by_instance( - $component, - $instancetype, - $course->id - ); - - // Test against the database. - $communicationuserrecord = $DB->get_record('communication_user', [ - 'commid' => $communicationprocessor->get_id(), - 'userid' => $userid - ]); - - $this->assertEquals($communicationuserrecord->userid, $userid); - $this->assertEquals($communicationuserrecord->commid, $communicationprocessor->get_id()); - - // Now add again. - $communicationprocessor->delete_instance_user_mapping([$userid]); - - // Test against the database. - $communicationuserrecord = $DB->get_record('communication_user', [ - 'commid' => $communicationprocessor->get_id(), - 'userid' => $userid - ]); - - $this->assertEmpty($communicationuserrecord); - } - - /** - * Test delete instance user mapping. - * - * @covers \core_communication\processor::create_instance_user_mapping - * @covers \core_communication\processor::mark_users_as_synced - * @covers \core_communication\processor::get_instance_userids - * @covers \core_communication\processor::delete_instance_user_mapping - */ - public function test_delete_instance_user_mapping(): void { - $this->resetAfterTest(); - - global $DB; - $course = $this->get_course('Sampleroom', 'none'); - $userid = $this->get_user()->id; - - // Sample data. - $communicationroomname = 'Sampleroom'; - $selectedcommunication = 'communication_matrix'; - $component = 'core_course'; - $instancetype = 'coursecommunication'; - - // First test the adding members to a room. - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $communication->create_and_configure_room($selectedcommunication, $communicationroomname); - $communication->add_members_to_room([$userid]); - - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - // Test against the object. - $communicationprocessor = processor::load_by_instance( - $component, - $instancetype, - $course->id - ); - - $this->assertEquals([$userid], $communicationprocessor->get_all_userids_for_instance()); - - // Delete the user mapping. - $communicationprocessor->delete_instance_user_mapping([$userid]); - - $this->assertEmpty($communicationprocessor->get_all_userids_for_instance()); - - // Test against the database. - $communicationuserrecord = $DB->get_record('communication_user', [ - 'commid' => $communicationprocessor->get_id(), - 'userid' => $userid - ]); - - $this->assertEmpty($communicationuserrecord); - } - - /** - * Test delete user mappings for instance. - * - * @covers \core_communication\processor::create_instance_user_mapping - * @covers \core_communication\processor::mark_users_as_synced - * @covers \core_communication\processor::get_instance_userids - * @covers \core_communication\processor::delete_user_mappings_for_instance - */ - public function test_delete_user_mappings_for_instance(): void { - $this->resetAfterTest(); - - global $DB; - $course = $this->get_course('Sampleroom', 'none'); - $userid = $this->get_user()->id; - - // Sample data. - $communicationroomname = 'Sampleroom'; - $selectedcommunication = 'communication_matrix'; - $component = 'core_course'; - $instancetype = 'coursecommunication'; - - // First test the adding members to a room. - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $communication->create_and_configure_room($selectedcommunication, $communicationroomname); - $communication->add_members_to_room([$userid]); - - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - $this->runAdhocTasks('\core_communication\task\add_members_to_room_task'); - - // Test against the object. - $communicationprocessor = processor::load_by_instance( - $component, - $instancetype, - $course->id - ); - - $this->assertEquals([$userid], $communicationprocessor->get_all_userids_for_instance()); - - // Delete the user mapping. - $communicationprocessor->delete_user_mappings_for_instance(); - - $this->assertEmpty($communicationprocessor->get_all_userids_for_instance()); - - // Test against the database. - $communicationuserrecord = $DB->get_record('communication_user', [ - 'commid' => $communicationprocessor->get_id(), - 'userid' => $userid - ]); - - $this->assertEmpty($communicationuserrecord); - } - - /** - * Test status notifications of a communication room are generated correctly. - * - * @covers \core_communication\api::show_communication_room_status_notification - */ - public function test_show_communication_room_status_notification(): void { - $course = $this->get_course(); - - // Get communication api object. - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - - // Room should be in 'pending' state before the task is run and show a notification. - $communication->show_communication_room_status_notification(); - $notifications = \core\notification::fetch(); - $this->assertStringContainsString('Your Matrix room will be ready soon.', $notifications[0]->get_message()); - - // Run the task. - $this->runAdhocTasks('\core_communication\task\create_and_configure_room_task'); - - // Get updated communication api after room configuration. - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - - // Check the room is now in 'ready' state and show a notification. - $communication->show_communication_room_status_notification(); - $notifications = \core\notification::fetch(); - $this->assertStringContainsString('Your Matrix room is ready!', $notifications[0]->get_message()); - } - - /** - * Test set provider data from handler. - * - * @covers \core_communication\api::set_data - * @covers \communication_matrix\communication_feature::set_form_data - */ - public function test_set_provider_data(): void { - $this->resetAfterTest(); - $course = $this->get_course(); - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - - // Sample data. - $roomname = 'Sampleroom'; - $provider = 'communication_matrix'; - - // Set the data. - $communication->set_data($course); - - // Test the set data. - $this->assertEquals($roomname, $course->communicationroomname); - $this->assertEquals($provider, $course->selectedcommunication); - } -} diff --git a/communication/provider/matrix/tests/matrix_events_manager_test.php b/communication/provider/matrix/tests/matrix_events_manager_test.php deleted file mode 100644 index 32bb3757e56..00000000000 --- a/communication/provider/matrix/tests/matrix_events_manager_test.php +++ /dev/null @@ -1,105 +0,0 @@ -. - -namespace communication_matrix; - -defined('MOODLE_INTERNAL') || die(); - -require_once(__DIR__ . '/matrix_test_helper_trait.php'); - -/** - * Class matrix_events_manager_test to test the matrix events endpoint. - * - * @package communication_matrix - * @category test - * @copyright 2023 Safat Shahin - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @coversDefaultClass \communication_matrix\matrix_events_manager - */ -class matrix_events_manager_test extends \advanced_testcase { - - use matrix_test_helper_trait; - - public function setUp(): void { - parent::setUp(); - $this->initialise_mock_server(); - } - - /** - * Test the api endpoints url's for matrix. - * - * @return void - * @covers ::get_token - * @covers ::get_update_avatar_endpoint - * @covers ::get_update_room_topic_endpoint - * @covers ::get_update_room_name_endpoint - * @covers ::get_create_room_endpoint - * @covers ::get_delete_room_endpoint - * @covers ::get_upload_content_endpoint - */ - public function test_matrix_api_endpoints(): void { - $this->resetAfterTest(); - $mockroomid = 'sampleroomid'; - $mockuserid = 'sampleuserid'; - - $matrixeventsmanager = new matrix_events_manager($mockroomid); - - // Test the endpoints and information. - $this->assertEquals($this->get_matrix_access_token(), $matrixeventsmanager->get_token()); - - $this->assertEquals($this->get_matrix_server_url() . '/' . '_matrix/client/r0/createRoom', - $matrixeventsmanager->get_create_room_endpoint()); - - $this->assertEquals($this->get_matrix_server_url() . '/' . '_matrix/client/r0/rooms' . - '/' . urlencode($mockroomid) . '/' . 'state/m.room.topic/', - $matrixeventsmanager->get_update_room_topic_endpoint()); - - $this->assertEquals($this->get_matrix_server_url(). '/' . '_matrix/client/r0/rooms' . - '/' . urlencode($mockroomid) . '/' . 'state/m.room.name/', - $matrixeventsmanager->get_update_room_name_endpoint()); - - $this->assertEquals($this->get_matrix_server_url(). '/' . '_synapse/admin/v1/rooms' . - '/' . urlencode($mockroomid), $matrixeventsmanager->get_room_info_endpoint()); - - $this->assertEquals($this->get_matrix_server_url() . '/' . '_synapse/admin/v1/rooms/' . urlencode($mockroomid), - $matrixeventsmanager->get_delete_room_endpoint()); - - $this->assertEquals($this->get_matrix_server_url() . '/' . '_matrix/media/r0/upload/', - $matrixeventsmanager->get_upload_content_endpoint()); - - $this->assertEquals($this->get_matrix_server_url() . '/' . '_matrix/client/r0/rooms' . - '/' . urlencode($mockroomid) . '/' . 'state/m.room.avatar/', - $matrixeventsmanager->get_update_avatar_endpoint()); - - $this->assertEquals($this->get_matrix_server_url() . '/' . '_matrix/client/r0/rooms' . - '/' . urlencode($mockroomid) . '/' . 'joined_members', - $matrixeventsmanager->get_room_membership_joined_endpoint()); - - $this->assertEquals($this->get_matrix_server_url() . '/' . '_synapse/admin/v1/join' . - '/' . urlencode($mockroomid), - $matrixeventsmanager->get_room_membership_join_endpoint()); - - $this->assertEquals($this->get_matrix_server_url() . '/' . '_matrix/client/r0/rooms' . - '/' . urlencode($mockroomid) . '/' . 'kick', - $matrixeventsmanager->get_room_membership_kick_endpoint()); - - $this->assertEquals($this->get_matrix_server_url() . '/' . '_synapse/admin/v2/users/' . urlencode($mockuserid), - $matrixeventsmanager->get_create_user_endpoint($mockuserid)); - - $this->assertEquals($this->get_matrix_server_url() . '/' . '_synapse/admin/v2/users/' . urlencode($mockuserid), - $matrixeventsmanager->get_user_info_endpoint($mockuserid)); - } -} diff --git a/communication/provider/matrix/tests/matrix_rooms_test.php b/communication/provider/matrix/tests/matrix_rooms_test.php index 783d6f4c7e9..f7f43c57bf1 100644 --- a/communication/provider/matrix/tests/matrix_rooms_test.php +++ b/communication/provider/matrix/tests/matrix_rooms_test.php @@ -16,14 +16,6 @@ namespace communication_matrix; -use core_communication\processor; -use core_communication\communication_test_helper_trait; - -defined('MOODLE_INTERNAL') || die(); - -require_once(__DIR__ . '/matrix_test_helper_trait.php'); -require_once(__DIR__ . '/../../../tests/communication_test_helper_trait.php'); - /** * Class matrix_rooms_test to test the matrix room data in db. * @@ -35,149 +27,106 @@ require_once(__DIR__ . '/../../../tests/communication_test_helper_trait.php'); */ class matrix_rooms_test extends \advanced_testcase { - use matrix_test_helper_trait; - use communication_test_helper_trait; + /** + * Test for load_by_processor_id with no record. + * + * @covers ::load_by_processor_id + */ + public function test_load_by_processor_id_none(): void { + $this->assertNull(matrix_rooms::load_by_processor_id(999999999)); + } - public function setUp(): void { - parent::setUp(); + /** + * Test for load_by_processor_id with valid records. + * + * @covers ::create_room_record + * @covers ::__construct + * @covers ::load_by_processor_id + * @covers ::get_processor_id + * @covers ::get_room_id + * @covers ::get_topic + */ + public function test_create_room_record(): void { $this->resetAfterTest(); - $this->setup_communication_configs(); + + $room = matrix_rooms::create_room_record( + processorid: 12345, + topic: 'The topic of this room is thusly', + ); + + $this->assertInstanceOf(matrix_rooms::class, $room); + $this->assertEquals(12345, $room->get_processor_id()); + $this->assertEquals('The topic of this room is thusly', $room->get_topic()); + $this->assertNull($room->get_room_id()); + + $room = matrix_rooms::create_room_record( + processorid: 54321, + topic: 'The topic of this room is thusly', + roomid: 'This is a roomid', + ); + + $this->assertInstanceOf(matrix_rooms::class, $room); + $this->assertEquals(54321, $room->get_processor_id()); + $this->assertEquals('The topic of this room is thusly', $room->get_topic()); + $this->assertEquals('This is a roomid', $room->get_room_id()); + + $reloadedroom = matrix_rooms::load_by_processor_id(54321); + $this->assertEquals(54321, $reloadedroom->get_processor_id()); + $this->assertEquals('The topic of this room is thusly', $reloadedroom->get_topic()); + $this->assertEquals('This is a roomid', $reloadedroom->get_room_id()); + } /** - * Test the matrix room creation in database. + * Test for update_room_record. * - * @covers ::create_matrix_room_record + * @covers ::update_room_record */ - public function test_create_matrix_room_record(): void { - global $DB; - $course = $this->get_course(); + public function test_update_room_record(): void { + $this->resetAfterTest(); - $sampleroomid = 'samplematrixroomid'; - $sampleroomtopic = 'samplematrixroomtopic'; - - // Communication internal api call. - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id + $room = matrix_rooms::create_room_record( + processorid: 12345, + topic: 'The topic of this room is that', ); - // Call matrix room object to create the matrix data. - $matrixroom = new \communication_matrix\matrix_rooms($communicationprocessor->get_id()); - $matrixroom->update_matrix_room_record( - $sampleroomid, - $sampleroomtopic + // Add a roomid. + $room->update_room_record( + roomid: 'This is a roomid', ); - // Test the object. - $this->assertEquals($matrixroom->get_matrix_room_id(), $sampleroomid); + $this->assertEquals('This is a roomid', $room->get_room_id()); + $this->assertEquals('The topic of this room is that', $room->get_topic()); + $this->assertEquals(12345, $room->get_processor_id()); - // Get the record from db. - $matrixrecord = $DB->get_record('matrix_rooms', - ['commid' => $communicationprocessor->get_id()]); + // Alter the roomid and topic. + $room->update_room_record( + roomid: 'updatedRoomId', + topic: 'updatedTopic is here', + ); - // Check the record against sample data. - $this->assertNotEmpty($matrixrecord); - $this->assertEquals($sampleroomid, $matrixrecord->roomid); - $this->assertEquals($communicationprocessor->get_id(), $matrixrecord->commid); + $this->assertEquals('updatedRoomId', $room->get_room_id()); + $this->assertEquals('updatedTopic is here', $room->get_topic()); + $this->assertEquals(12345, $room->get_processor_id()); } /** - * Test matrix room record updates. + * Tests for delete_room_record. * - * @covers ::update_matrix_room_record + * @covers ::delete_room_record */ - public function test_update_matrix_room_record(): void { + public function test_delete_room_record(): void { global $DB; - $course = $this->get_course(); - $sampleroomid = 'samplematrixroomid'; - $sampleroomtopic = 'samplematrixroomtopic'; + $this->resetAfterTest(); - // Communication internal api call. - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id + $room = matrix_rooms::create_room_record( + processorid: 12345, + topic: 'The topic of this room is that', ); + $this->assertCount(1, $DB->get_records('matrix_rooms')); - // Call matrix room object to create the matrix data. - $matrixroom = new \communication_matrix\matrix_rooms($communicationprocessor->get_id()); - $matrixroom->update_matrix_room_record( - $sampleroomid, - $sampleroomtopic - ); - - // Get the record from db. - $matrixrecord = $DB->get_record('matrix_rooms', - ['commid' => $communicationprocessor->get_id()]); - - // Check the record against sample data. - $this->assertNotEmpty($matrixrecord); - - $sampleroomidupdated = 'samplematrixroomidupdated'; - - $matrixroom->update_matrix_room_record( - $sampleroomidupdated, - $sampleroomtopic - ); - - // Test the object. - $this->assertEquals($matrixroom->get_matrix_room_id(), $sampleroomidupdated); - - // Get the record from db. - $matrixrecord = $DB->get_record('matrix_rooms', - ['commid' => $communicationprocessor->get_id()]); - - // Check the record against sample data. - $this->assertNotEmpty($matrixrecord); - $this->assertEquals($sampleroomidupdated, $matrixrecord->roomid); - $this->assertEquals($communicationprocessor->get_id(), $matrixrecord->commid); - } - - /** - * Test matrix room deletion. - * - * @covers ::delete_matrix_room_record - * @covers ::get_matrix_room_id - */ - public function test_delete_matrix_room_record(): void { - global $DB; - $course = $this->get_course(); - - $sampleroomid = 'samplematrixroomid'; - $sampleroomtopic = 'samplematrixroomtopic'; - - // Communication internal api call. - $communicationprocessor = processor::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - - // Call matrix room object to create the matrix data. - $matrixroom = new \communication_matrix\matrix_rooms($communicationprocessor->get_id()); - $matrixroom->update_matrix_room_record( - $sampleroomid, - $sampleroomtopic - ); - - // Get the record from db. - $matrixrecord = $DB->get_record('matrix_rooms', - ['commid' => $communicationprocessor->get_id()]); - - // Check the record against sample data. - $this->assertNotEmpty($matrixrecord); - - // Now delete the record. - $matrixroom->delete_matrix_room_record(); - - // Get the record from db. - $matrixrecord = $DB->get_record('matrix_rooms', - ['commid' => $communicationprocessor->get_id()]); - - // Check the record against sample data. - $this->assertEmpty($matrixrecord); + $room->delete_room_record(); + $this->assertCount(0, $DB->get_records('matrix_rooms')); } } diff --git a/communication/provider/matrix/tests/matrix_test_helper_trait.php b/communication/provider/matrix/tests/matrix_test_helper_trait.php index 80d69b4f30a..8aa7bbd942b 100644 --- a/communication/provider/matrix/tests/matrix_test_helper_trait.php +++ b/communication/provider/matrix/tests/matrix_test_helper_trait.php @@ -16,6 +16,8 @@ namespace communication_matrix; +use GuzzleHttp\Psr7\Response; + /** * Trait matrix_helper_trait to generate initial setup for matrix mock and associated helpers. * @@ -112,9 +114,12 @@ trait matrix_test_helper_trait { * @return \stdClass */ public function get_matrix_room_data(string $roomid): \stdClass { - $matrixeventmanager = new matrix_events_manager($roomid); - $response = $matrixeventmanager->request()->get($matrixeventmanager->get_room_info_endpoint()); - return json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR); + $rooms = $this->backoffice_get_all_rooms(); + foreach ($rooms as $room) { + if ($room->room_id === $roomid) { + return $room; + } + } } /** @@ -125,9 +130,41 @@ trait matrix_test_helper_trait { * @return \stdClass */ public function get_matrix_user_data(string $roomid, string $matrixuserid): \stdClass { - $matrixeventmanager = new matrix_events_manager($roomid); - $response = $matrixeventmanager->request()->get($matrixeventmanager->get_user_info_endpoint($matrixuserid)); - return json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR); + $users = $this->backoffice_get_all_users(); + + foreach ($users as $user) { + if ($user->userid === $matrixuserid) { + return $user; + } + } + } + + /** + * A backoffice call to get all registered users from our mock server. + * + * @return array + */ + public function backoffice_get_all_users(): array { + $client = new \core\http_client(); + + return json_decode($client->get($this->get_backoffice_uri('users'))->getBody())->users; + } + + /** + * A backoffice method to create users and rooms on our mock server. + * + * @param array $users + * @param array $rooms + */ + public function backoffice_create_users_and_rooms( + array $users = [], + array $rooms = [], + ): Response { + $client = new \core\http_client(); + return $client->put($this->get_backoffice_uri('create'), ['json' => [ + 'users' => $users, + 'rooms' => $rooms, + ]]); } /** @@ -138,7 +175,7 @@ trait matrix_test_helper_trait { * @return \core\http_client */ public function request(array $jsonarray = [], array $headers = []): \core\http_client { - $response = new \core\http_client([ + $response = new \core\http_client([ 'headers' => $headers, 'json' => $jsonarray, ]); diff --git a/communication/provider/matrix/tests/matrix_user_manager_test.php b/communication/provider/matrix/tests/matrix_user_manager_test.php index 2d7a83dd6aa..4d97be6bb83 100644 --- a/communication/provider/matrix/tests/matrix_user_manager_test.php +++ b/communication/provider/matrix/tests/matrix_user_manager_test.php @@ -35,7 +35,6 @@ class matrix_user_manager_test extends \advanced_testcase { public function test_get_matrixid_from_moodle_without_field(): void { $user = get_admin(); - // And confirm that they're fetched back. $this->assertNull(matrix_user_manager::get_matrixid_from_moodle($user->id)); } diff --git a/communication/tests/api_test.php b/communication/tests/api_test.php index 9b5b0fd3d48..ca1787a4b43 100644 --- a/communication/tests/api_test.php +++ b/communication/tests/api_test.php @@ -27,7 +27,7 @@ require_once(__DIR__ . '/communication_test_helper_trait.php'); * @category test * @copyright 2023 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @coversDefaultClass \core_communication\api + * @covers \core_communication\api */ class api_test extends \advanced_testcase { @@ -41,8 +41,6 @@ class api_test extends \advanced_testcase { /** * Test the communication plugin list for the form element returns the correct number of plugins. - * - * @covers ::get_communication_plugin_list_for_form */ public function test_get_communication_plugin_list_for_form(): void { $communicationplugins = \core_communication\api::get_communication_plugin_list_for_form(); @@ -54,8 +52,6 @@ class api_test extends \advanced_testcase { /** * Test set data to the instance. - * - * @covers ::set_data */ public function test_set_data(): void { $course = $this->get_course(); @@ -80,8 +76,6 @@ class api_test extends \advanced_testcase { /** * Test get_current_communication_provider method. - * - * @covers ::get_provider */ public function test_get_provider(): void { $course = $this->get_course(); @@ -95,31 +89,8 @@ class api_test extends \advanced_testcase { $this->assertEquals('communication_matrix', $communication->get_provider()); } - /** - * Test get_avatar_filerecord method. - * - * @covers ::get_avatar_filerecord - */ - public function test_get_avatar_filerecord(): void { - $course = $this->get_course(); - - $communication = \core_communication\api::load_by_instance( - 'core_course', - 'coursecommunication', - $course->id - ); - $filerecord = $communication->get_avatar_filerecord('avatar.svg'); - - $this->assertEquals('avatar.svg', $filerecord->filename); - $this->assertEquals('core_communication', $filerecord->component); - $this->assertEquals('avatar', $filerecord->filearea); - } - /** * Test set_avatar method. - * - * @covers ::set_avatar - * @covers ::get_avatar_filerecord */ public function test_set_avatar(): void { global $CFG; @@ -135,26 +106,31 @@ class api_test extends \advanced_testcase { 'moodle_logo.jpg', ); + // Create the room, settingthe avatar. $communication = \core_communication\api::load_by_instance( 'core_course', 'coursecommunication', - $course->id + $course->id, ); + $communication->create_and_configure_room($selectedcommunication, $communicationroomname, $avatar); + // Reload the communication processor. $communicationprocessor = processor::load_by_instance( 'core_course', 'coursecommunication', - $course->id + $course->id, ); - $this->assertNotNull($communicationprocessor->get_avatar()); + // Compare result. + $this->assertEquals( + $avatar->get_contenthash(), + $communicationprocessor->get_avatar()->get_contenthash(), + ); } /** * Test the create_and_configure_room method to add/create tasks. - * - * @covers ::create_and_configure_room */ public function test_create_and_configure_room(): void { // Get the course by disabling communication so that we can create it manually calling the api. @@ -191,8 +167,6 @@ class api_test extends \advanced_testcase { /** * Test the create_and_configure_room method to add/create tasks when no communication provider selected. - * - * @covers ::create_and_configure_room */ public function test_create_and_configure_room_without_communication_provider_selected(): void { // Get the course by disabling communication so that we can create it manually calling the api. @@ -214,8 +188,6 @@ class api_test extends \advanced_testcase { /** * Test update operation. - * - * @covers ::update_room */ public function test_update_room(): void { $course = $this->get_course(); @@ -231,14 +203,6 @@ class api_test extends \advanced_testcase { ); $communication->update_room($selectedcommunication, $communicationroomname); - // Test the tasks added. - $adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\update_room_task'); - // Should be 2 as one for create, another for update. - $this->assertCount(1, $adhoctask); - - $adhoctask = reset($adhoctask); - $this->assertInstanceOf('\\core_communication\\task\\update_room_task', $adhoctask); - // Test the communication record exists. $communicationprocessor = processor::load_by_instance( 'core_course', @@ -252,8 +216,6 @@ class api_test extends \advanced_testcase { /** * Test delete operation. - * - * @covers ::delete_room */ public function test_delete_room(): void { $course = $this->get_course(); @@ -290,9 +252,6 @@ class api_test extends \advanced_testcase { /** * Test the update_room_membership for adding adn removing members. - * - * @covers ::add_members_to_room - * @covers ::remove_members_from_room */ public function test_update_room_membership(): void { $course = $this->get_course(); @@ -320,8 +279,6 @@ class api_test extends \advanced_testcase { /** * Test the enabled communication plugin list and default. - * - * @covers ::get_enabled_providers_and_default */ public function test_get_enabled_providers_and_default(): void { list($communicationproviders, $defaulprovider) = \core_communication\api::get_enabled_providers_and_default(); diff --git a/course/lib.php b/course/lib.php index 2fe83a0eaf2..a31906558d0 100644 --- a/course/lib.php +++ b/course/lib.php @@ -2345,13 +2345,19 @@ function create_course($data, $editoroptions = NULL) { // Prepare the communication api data. $courseimage = course_get_courseimage($course); $communicationroomname = !empty($data->communicationroomname) ? $data->communicationroomname : $data->fullname; + // Communication api call. $communication = \core_communication\api::load_by_instance( 'core_course', 'coursecommunication', - $course->id + $course->id, + ); + $communication->create_and_configure_room( + $provider, + $communicationroomname, + $courseimage ?: null, + $data, ); - $communication->create_and_configure_room($provider, $communicationroomname, $courseimage, $data); } }