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); } }