diff --git a/communication/classes/admin/manage_communication_providers_page.php b/communication/classes/admin/manage_communication_providers_page.php index 384f8d2b12a..d84c4defae5 100644 --- a/communication/classes/admin/manage_communication_providers_page.php +++ b/communication/classes/admin/manage_communication_providers_page.php @@ -32,11 +32,14 @@ use moodle_url; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manage_communication_providers_page extends admin_setting { - public function __construct() { $this->nosave = true; - parent::__construct('managecommunications', - new \lang_string('managecommunicationproviders', 'core_communication'), '', ''); + parent::__construct( + 'managecommunications', + new \lang_string('managecommunicationproviders', 'core_communication'), + '', + '' + ); } public function get_setting(): bool { @@ -71,20 +74,26 @@ class manage_communication_providers_page extends admin_setting { foreach ($plugins as $plugin) { $class = ''; $actionurl = new moodle_url('/admin/communication.php', ['sesskey' => sesskey(), 'name' => $plugin->name]); - if ($pluginmanager->get_plugin_info('communication_' . $plugin->name)->get_status() === - core_plugin_manager::PLUGIN_STATUS_MISSING) { + if ( + $pluginmanager->get_plugin_info('communication_' . $plugin->name)->get_status() === + core_plugin_manager::PLUGIN_STATUS_MISSING + ) { $strtypename = $plugin->displayname . ' (' . get_string('missingfromdisk') . ')'; } else { $strtypename = $plugin->displayname; } if ($plugin->is_enabled()) { - $hideshow = html_writer::link($actionurl->out(false, ['action' => 'disable']), - $OUTPUT->pix_icon('t/hide', get_string('disable'), 'moodle', ['class' => 'iconsmall'])); + $hideshow = html_writer::link( + $actionurl->out(false, ['action' => 'disable']), + $OUTPUT->pix_icon('t/hide', get_string('disable'), 'moodle', ['class' => 'iconsmall']) + ); } else { $class = 'dimmed_text'; - $hideshow = html_writer::link($actionurl->out(false, ['action' => 'enable']), - $OUTPUT->pix_icon('t/show', get_string('enable'), 'moodle', ['class' => 'iconsmall'])); + $hideshow = html_writer::link( + $actionurl->out(false, ['action' => 'enable']), + $OUTPUT->pix_icon('t/show', get_string('enable'), 'moodle', ['class' => 'iconsmall']) + ); } $settings = ''; @@ -93,8 +102,12 @@ class manage_communication_providers_page extends admin_setting { } $uninstall = ''; - if ($uninstallurl = core_plugin_manager::instance()->get_uninstall_url( - 'communication_' . $plugin->name, 'manage')) { + if ( + $uninstallurl = core_plugin_manager::instance()->get_uninstall_url( + 'communication_' . $plugin->name, + 'manage' + ) + ) { $uninstall = html_writer::link($uninstallurl, get_string('uninstallplugin', 'core_admin')); } @@ -114,8 +127,10 @@ class manage_communication_providers_page extends admin_setting { } $types = core_plugin_manager::instance()->get_plugins_of_type('communication'); foreach ($types as $type) { - if (strpos($type->component, $query) !== false || - strpos(core_text::strtolower($type->displayname), $query) !== false) { + if ( + strpos($type->component, $query) !== false || + strpos(core_text::strtolower($type->displayname), $query) !== false + ) { return true; } } diff --git a/communication/classes/api.php b/communication/classes/api.php index e7ee4c7fe96..2fc9162f5c7 100644 --- a/communication/classes/api.php +++ b/communication/classes/api.php @@ -21,6 +21,7 @@ use core_communication\task\create_and_configure_room_task; use core_communication\task\delete_room_task; use core_communication\task\remove_members_from_room; use core_communication\task\update_room_task; +use core_communication\task\update_room_membership_task; use stdClass; /** @@ -39,7 +40,6 @@ use stdClass; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { - /** * @var null|processor $communication The communication settings object */ @@ -166,7 +166,7 @@ class api { $selection[processor::PROVIDER_NONE] = get_string('nocommunicationselected', 'communication'); $communicationplugins = \core\plugininfo\communication::get_enabled_plugins(); foreach ($communicationplugins as $pluginname => $notusing) { - $selection['communication_' . $pluginname] = get_string('pluginname', 'communication_'. $pluginname); + $selection['communication_' . $pluginname] = get_string('pluginname', 'communication_' . $pluginname); } return $selection; } @@ -199,8 +199,7 @@ class api { ): void { global $PAGE; - list($communicationproviders, $defaulprovider) = self:: - get_enabled_providers_and_default($selectdefaultcommunication); + [$communicationproviders, $defaulprovider] = self::get_enabled_providers_and_default($selectdefaultcommunication); $PAGE->requires->js_call_amd('core_communication/providerchooser', 'init'); @@ -216,10 +215,12 @@ class api { $mform->setDefault('selectedcommunication', $defaulprovider); $mform->registerNoSubmitButton('updatecommunicationprovider'); - $mform->addElement('submit', + $mform->addElement( + 'submit', 'updatecommunicationprovider', 'update communication', - ['data-communicationchooser-field' => 'updateButton', 'class' => 'd-none',]); + ['data-communicationchooser-field' => 'updateButton', 'class' => 'd-none'] + ); // Just a placeholder for the communication options. $mform->addElement('hidden', 'addcommunicationoptionshere'); @@ -239,7 +240,9 @@ class api { $mform->createElement( 'text', 'communicationroomname', - get_string('communicationroomname', 'communication'), 'maxlength="100" size="20"'), + get_string('communicationroomname', 'communication'), + 'maxlength="100" size="20"' + ), 'addcommunicationoptionshere' ); $mform->addHelpButton('communicationroomname', 'communicationroomname', 'communication'); @@ -247,7 +250,6 @@ class api { processor::set_provider_specific_form_definition($provider, $mform); } - } /** @@ -529,6 +531,34 @@ class api { } } + /** + * Create a communication ad-hoc task for updating members operation and update the user mapping. + * + * This method will add a task to the queue to update the room users. + * + * @param array $userids The user ids to add to the room + * @param bool $queue Whether to queue the task or not + */ + public function update_room_membership(array $userids, bool $queue = true): void { + // No communication object? something not done right. + if (!$this->communication) { + return; + } + + // No userids? don't bother doing anything. + if (empty($userids)) { + return; + } + + $this->communication->reset_users_sync_flag($userids); + + if ($queue) { + update_room_membership_task::queue( + $this->communication + ); + } + } + /** * Create a communication ad-hoc task for remove members operation or action immediately. * @@ -582,7 +612,6 @@ class api { switch ($roomstatus) { case 'pending': - \core\notification::add($message, \core\notification::INFO); break; diff --git a/communication/classes/communication_provider.php b/communication/classes/communication_provider.php index 3fa7127a18d..2023ee3cb5e 100644 --- a/communication/classes/communication_provider.php +++ b/communication/classes/communication_provider.php @@ -28,7 +28,6 @@ namespace core_communication; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface communication_provider { - /** * A base communication provider. * diff --git a/communication/classes/form/configure_form.php b/communication/classes/form/configure_form.php index 6b6fa251483..d01bf324c8f 100644 --- a/communication/classes/form/configure_form.php +++ b/communication/classes/form/configure_form.php @@ -26,13 +26,12 @@ namespace core_communication\form; defined('MOODLE_INTERNAL') || die(); -require_once($CFG->libdir.'/formslib.php'); +require_once($CFG->libdir . '/formslib.php'); /** * Defines the configure communication form. */ class configure_form extends \moodleform { - /** * @var \core_communication\api $communication The communication api object. */ diff --git a/communication/classes/form_provider.php b/communication/classes/form_provider.php index 052608ae675..a4c535d5e6d 100644 --- a/communication/classes/form_provider.php +++ b/communication/classes/form_provider.php @@ -26,7 +26,6 @@ namespace core_communication; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface form_provider { - /** * Set the form data to the instance if any data is available. * diff --git a/communication/classes/privacy/provider.php b/communication/classes/privacy/provider.php index ccc426407f0..9c7f4d1659a 100644 --- a/communication/classes/privacy/provider.php +++ b/communication/classes/privacy/provider.php @@ -29,10 +29,10 @@ use core_privacy\local\request\userlist; * @copyright 2023 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class provider implements \core_privacy\local\metadata\provider, - \core_privacy\local\request\subsystem\provider, - \core_privacy\local\request\core_userlist_provider { - +class provider implements + \core_privacy\local\request\core_userlist_provider, + \core_privacy\local\metadata\provider, + \core_privacy\local\request\subsystem\provider { public static function get_metadata(collection $collection): collection { $collection->add_database_table('communication_user', [ diff --git a/communication/classes/processor.php b/communication/classes/processor.php index 40b96ba24be..9ced9f1e1f8 100644 --- a/communication/classes/processor.php +++ b/communication/classes/processor.php @@ -29,7 +29,6 @@ use stored_file; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class processor { - /** @var string The magic 'none' provider */ public const PROVIDER_NONE = 'none'; @@ -171,6 +170,21 @@ class processor { ); } + /** + * Get all the user ids flagged as deleted. + * + * @return array + */ + public function get_all_delete_flagged_userids(): array { + global $DB; + return $DB->get_fieldset_select( + 'communication_user', + 'userid', + 'commid = ? AND deleted = ?', + [$this->instancedata->id, 1] + ); + } + /** * Create communication user record for mapping and sync. * @@ -309,7 +323,7 @@ class processor { $DB->delete_records_select( 'communication_user', - 'commid = ? AND userid IN (' . implode(',', $userids) . ') AND synced = ?' , + 'commid = ? AND userid IN (' . implode(',', $userids) . ') AND synced = ?', [$this->instancedata->id, 0] ); } @@ -400,6 +414,15 @@ class processor { /** * Get communication instance id. * + * @return int + */ + public function get_instance_id(): int { + return $this->instancedata->instanceid; + } + + /** + * Get communication instance component. + * * @return string */ public function get_component(): string { diff --git a/communication/classes/room_chat_provider.php b/communication/classes/room_chat_provider.php index 234b1c2fc8b..7e1e127a510 100644 --- a/communication/classes/room_chat_provider.php +++ b/communication/classes/room_chat_provider.php @@ -26,7 +26,6 @@ namespace core_communication; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface room_chat_provider { - /** * Create a provider room when a instance is created. */ diff --git a/communication/classes/room_user_provider.php b/communication/classes/room_user_provider.php index f9a04e48a4b..f055c706365 100644 --- a/communication/classes/room_user_provider.php +++ b/communication/classes/room_user_provider.php @@ -31,6 +31,13 @@ interface room_user_provider { */ public function add_members_to_room(array $userids): void; + /** + * Update room membership for the communication room. + * + * @param array $userids The user ids to be updated + */ + public function update_room_membership(array $userids): void; + /** * Remove members from room. * diff --git a/communication/classes/task/add_members_to_room_task.php b/communication/classes/task/add_members_to_room_task.php index b49931c6b18..f11cbc04bb5 100644 --- a/communication/classes/task/add_members_to_room_task.php +++ b/communication/classes/task/add_members_to_room_task.php @@ -27,7 +27,6 @@ use core_communication\processor; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class add_members_to_room_task extends adhoc_task { - public function execute() { // Initialize the custom data operation to be used for the action. $data = $this->get_custom_data(); @@ -55,7 +54,7 @@ class add_members_to_room_task extends adhoc_task { // Add ad-hoc task to update the provider room. $task = new self(); $task->set_custom_data([ - 'id' => $communication->get_id() + 'id' => $communication->get_id(), ]); // Queue the task for the next run. diff --git a/communication/classes/task/create_and_configure_room_task.php b/communication/classes/task/create_and_configure_room_task.php index a65dfee5d46..1646eef8d82 100644 --- a/communication/classes/task/create_and_configure_room_task.php +++ b/communication/classes/task/create_and_configure_room_task.php @@ -29,7 +29,6 @@ use core_communication\processor; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class create_and_configure_room_task extends adhoc_task { - public function execute() { $data = $this->get_custom_data(); @@ -66,7 +65,7 @@ class create_and_configure_room_task extends adhoc_task { // Add ad-hoc task to update the provider room. $task = new self(); $task->set_custom_data([ - 'id' => $communication->get_id() + 'id' => $communication->get_id(), ]); // Queue the task for the next run. diff --git a/communication/classes/task/delete_room_task.php b/communication/classes/task/delete_room_task.php index fea70c04a68..d8cb3eb9445 100644 --- a/communication/classes/task/delete_room_task.php +++ b/communication/classes/task/delete_room_task.php @@ -29,7 +29,6 @@ use core_communication\processor; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class delete_room_task extends adhoc_task { - public function execute() { $data = $this->get_custom_data(); @@ -63,7 +62,7 @@ class delete_room_task extends adhoc_task { // Add ad-hoc task to update the provider room. $task = new self(); $task->set_custom_data([ - 'id' => $communication->get_id() + 'id' => $communication->get_id(), ]); // Queue the task for the next run. diff --git a/communication/classes/task/remove_members_from_room.php b/communication/classes/task/remove_members_from_room.php index e681f949e01..1d6d444c3db 100644 --- a/communication/classes/task/remove_members_from_room.php +++ b/communication/classes/task/remove_members_from_room.php @@ -27,7 +27,6 @@ use core_communication\processor; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class remove_members_from_room extends adhoc_task { - public function execute() { // Initialize the custom data operation to be used for the action. $data = $this->get_custom_data(); @@ -40,7 +39,7 @@ class remove_members_from_room extends adhoc_task { return; } - $communication->get_room_user_provider()->remove_members_from_room($communication->get_instance_userids(true, true)); + $communication->get_room_user_provider()->remove_members_from_room($communication->get_all_delete_flagged_userids()); // Now remove any mapping for users who are not in the room. $communication->delete_instance_non_synced_user_mapping($communication->get_instance_userids(false, true)); @@ -58,7 +57,7 @@ class remove_members_from_room extends adhoc_task { // Add ad-hoc task to update the provider room. $task = new self(); $task->set_custom_data([ - 'id' => $communication->get_id() + 'id' => $communication->get_id(), ]); // Queue the task for the next run. diff --git a/communication/classes/task/update_room_membership_task.php b/communication/classes/task/update_room_membership_task.php new file mode 100644 index 00000000000..eb62f09ceb0 --- /dev/null +++ b/communication/classes/task/update_room_membership_task.php @@ -0,0 +1,63 @@ +. + +namespace core_communication\task; + +use core\task\adhoc_task; +use core_communication\processor; + +/** + * Class update_room_membership_task to add the task to update members for the room and execute the task to action the addition. + * + * @package core_communication + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class update_room_membership_task extends adhoc_task { + public function execute() { + // Initialize the custom data operation to be used for the action. + $data = $this->get_custom_data(); + + // Call the communication api to action the operation. + $communication = processor::load_by_id($data->id); + + if ($communication === null) { + mtrace("Skipping room creation because the instance does not exist"); + return; + } + + $communication->get_room_user_provider()->update_room_membership($communication->get_instance_userids()); + } + + /** + * Queue the task for the next run. + * + * @param processor $communication The communication processor to perform the action on + */ + public static function queue( + processor $communication + ): void { + + // Add ad-hoc task to update the provider room. + $task = new self(); + $task->set_custom_data([ + 'id' => $communication->get_id(), + ]); + + // Queue the task for the next run. + \core\task\manager::queue_adhoc_task($task); + } +} diff --git a/communication/classes/task/update_room_task.php b/communication/classes/task/update_room_task.php index b487e28e4d9..bb2cb18edbb 100644 --- a/communication/classes/task/update_room_task.php +++ b/communication/classes/task/update_room_task.php @@ -29,7 +29,6 @@ use core_communication\processor; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class update_room_task extends adhoc_task { - public function execute() { $data = $this->get_custom_data(); @@ -56,7 +55,7 @@ class update_room_task extends adhoc_task { // Add ad-hoc task to update the provider room. $task = new self(); $task->set_custom_data([ - 'id' => $communication->get_id() + 'id' => $communication->get_id(), ]); // Queue the task for the next run. diff --git a/communication/classes/user_provider.php b/communication/classes/user_provider.php index d59df679f61..4817f1d9a5e 100644 --- a/communication/classes/user_provider.php +++ b/communication/classes/user_provider.php @@ -24,7 +24,6 @@ namespace core_communication; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface user_provider { - /** * Create members. * diff --git a/communication/configure.php b/communication/configure.php index dd3ce0f987f..6043dbe2fa5 100644 --- a/communication/configure.php +++ b/communication/configure.php @@ -52,7 +52,7 @@ if (!$communication) { } // Set variables according to the component callback and use them on the page. -list($instance, $context, $heading, $returnurl) = component_callback( +[$instance, $context, $heading, $returnurl] = component_callback( $component, 'get_communication_instance_data', [$instanceid] @@ -79,11 +79,8 @@ $form = new \core_communication\form\configure_form( if ($form->is_cancelled()) { - redirect($returnurl); - } else if ($data = $form->get_data()) { - component_callback($component, 'update_communication_instance_data', [$data]); redirect($returnurl); } diff --git a/communication/provider/customlink/classes/communication_feature.php b/communication/provider/customlink/classes/communication_feature.php index a85a320a72a..3250be4a60a 100644 --- a/communication/provider/customlink/classes/communication_feature.php +++ b/communication/provider/customlink/classes/communication_feature.php @@ -27,9 +27,8 @@ use core_communication\processor; */ class communication_feature implements \core_communication\communication_provider, - \core_communication\room_chat_provider, - \core_communication\form_provider { - + \core_communication\form_provider, + \core_communication\room_chat_provider { /** @var string The database table storing custom link specific data */ protected const CUSTOMLINK_TABLE = 'communication_customlink'; @@ -132,7 +131,6 @@ class communication_feature implements // Create the record if it does not exist. $newrecord->commid = $commid; $DB->insert_record(self::CUSTOMLINK_TABLE, $newrecord); - } else if ($newrecord->url !== $existingrecord->url) { // Update record if the URL has changed. $newrecord->id = $existingrecord->id; @@ -154,15 +152,22 @@ class communication_feature implements public static function set_form_definition(\MoodleQuickForm $mform): void { // Custom link description for the communication provider. - $mform->insertElementBefore($mform->createElement('text', 'customlinkurl', + $mform->insertElementBefore($mform->createElement( + 'text', + 'customlinkurl', get_string('customlinkurl', 'communication_customlink'), - 'maxlength="255" size="40"'), 'addcommunicationoptionshere'); + 'maxlength="255" size="40"' + ), 'addcommunicationoptionshere'); $mform->addHelpButton('customlinkurl', 'customlinkurl', 'communication_customlink'); $mform->setType('customlinkurl', PARAM_URL); $mform->addRule('customlinkurl', get_string('required'), 'required', null, 'client'); $mform->addRule('customlinkurl', get_string('maximumchars', '', 255), 'maxlength', 255); - $mform->insertElementBefore($mform->createElement('static', 'customlinkurlinfo', '', + $mform->insertElementBefore($mform->createElement( + 'static', + 'customlinkurlinfo', + '', get_string('customlinkurlinfo', 'communication_customlink'), - 'addcommunicationoptionshere'), 'addcommunicationoptionshere'); + 'addcommunicationoptionshere' + ), 'addcommunicationoptionshere'); } } diff --git a/communication/provider/customlink/classes/privacy/provider.php b/communication/provider/customlink/classes/privacy/provider.php index 898502d85e7..f2b18cc92d0 100644 --- a/communication/provider/customlink/classes/privacy/provider.php +++ b/communication/provider/customlink/classes/privacy/provider.php @@ -26,7 +26,6 @@ use core_privacy\local\metadata\null_provider; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements null_provider { - /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. diff --git a/communication/provider/customlink/tests/communication_feature_test.php b/communication/provider/customlink/tests/communication_feature_test.php index 60d0cbd21cd..e3957e38c4f 100644 --- a/communication/provider/customlink/tests/communication_feature_test.php +++ b/communication/provider/customlink/tests/communication_feature_test.php @@ -33,7 +33,6 @@ require_once(__DIR__ . '/../../../tests/communication_test_helper_trait.php'); * @coversDefaultClass \communication_customlink\communication_feature */ class communication_feature_test extends \advanced_testcase { - use communication_test_helper_trait; public function setUp(): void { diff --git a/communication/provider/matrix/classes/communication_feature.php b/communication/provider/matrix/classes/communication_feature.php index 81637438425..92cf7a00fd1 100644 --- a/communication/provider/matrix/classes/communication_feature.php +++ b/communication/provider/matrix/classes/communication_feature.php @@ -45,12 +45,11 @@ use GuzzleHttp\Psr7\Response; */ class communication_feature implements \core_communication\communication_provider, - \core_communication\user_provider, + \core_communication\form_provider, \core_communication\room_chat_provider, \core_communication\room_user_provider, - \core_communication\form_provider { - - /** @var matrix_room $room The matrix room object to update room information */ + \core_communication\user_provider { + /** @var ?matrix_room $room The matrix room object to update room information */ private ?matrix_room $room = null; /** @var string|null The URI of the home server */ @@ -127,10 +126,7 @@ class communication_feature implements * @return null|matrix_room */ public function get_room_configuration(): ?matrix_room { - if ($this->room === null) { - $this->room = matrix_room::load_by_processor_id($this->processor->get_id()); - } - + $this->room = matrix_room::load_by_processor_id($this->processor->get_id()); return $this->room; } @@ -166,10 +162,10 @@ class communication_feature implements $response = $this->matrixapi->create_user( userid: $qualifiedmuid, displayname: $userfullname, - threepids: [(object)[ + threepids: [(object) [ 'medium' => 'email', - 'address' => $user->email - ]], + 'address' => $user->email, + ], ], externalids: [], ); $body = json_decode($response->getBody()); @@ -184,10 +180,19 @@ class communication_feature implements } } + // Set the power level of the users. + if (!empty($addedmembers) && $this->is_power_levels_update_required($addedmembers)) { + $this->set_matrix_power_levels(); + } + // Mark then users as synced for the added members. $this->processor->mark_users_as_synced($addedmembers); } + public function update_room_membership(array $userids): void { + $this->set_matrix_power_levels(); + } + /** * Add members to a room. * @@ -209,6 +214,11 @@ class communication_feature implements } } + // Set the power level of the users. + if (!empty($addedmembers) && $this->is_power_levels_update_required($addedmembers)) { + $this->set_matrix_power_levels(); + } + // Mark then users as synced for the added members. $this->processor->mark_users_as_synced($addedmembers); @@ -229,8 +239,8 @@ class communication_feature implements if (!$this->check_room_membership($matrixuserid)) { $response = $this->matrixapi->invite_member_to_room( - $this->get_room_id(), - $matrixuserid, + roomid: $this->get_room_id(), + userid: $matrixuserid, ); $body = self::get_body($response); @@ -256,25 +266,41 @@ class communication_feature implements // This API requiures the remove_members_from_room feature. $this->matrixapi->require_feature(remove_member_from_room_feature::class); + if ($this->get_room_id() === null) { + return; + } + + // Remove the power level for the user first. + $this->set_matrix_power_levels($userids); + $membersremoved = []; + $currentpowerlevels = $this->get_current_powerlevel_data(); + $currentuserpowerlevels = (array) $currentpowerlevels->users ?? []; + foreach ($userids as $userid) { // Check user is member of room first. $matrixuserid = matrix_user_manager::get_matrixid_from_moodle($userid); - // Check if user is the room admin and halt removal of this user. - $response = $this->matrixapi->get_room_info($this->get_room_id()); - $matrixroomdata = self::get_body($response); - $roomadmin = $matrixroomdata->creator; - $isadmin = $matrixuserid === $roomadmin; + if (!$matrixuserid) { + // Unable to find a matrix userid for this user. + continue; + } + + if (array_key_exists($matrixuserid, $currentuserpowerlevels)) { + if ($currentuserpowerlevels[$matrixuserid] >= matrix_constants::POWER_LEVEL_MAXIMUM) { + // Skip removing the user if they are an admin. + continue; + } + } if ( - !$isadmin && $matrixuserid && $this->check_user_exists($matrixuserid) && + $this->check_user_exists($matrixuserid) && $this->check_room_membership($matrixuserid) ) { $this->matrixapi->remove_member_from_room( - $this->get_room_id(), - $matrixuserid, + roomid: $this->get_room_id(), + userid: $matrixuserid, ); $membersremoved[] = $userid; @@ -295,7 +321,9 @@ class communication_feature implements // This API requires the get_user_info feature. $this->matrixapi->require_feature(get_user_info_feature::class); - $response = $this->matrixapi->get_user_info($matrixuserid); + $response = $this->matrixapi->get_user_info( + userid: $matrixuserid, + ); $body = self::get_body($response); return isset($body->name); @@ -312,7 +340,9 @@ class communication_feature implements // 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()); + $response = $this->matrixapi->get_room_members( + roomid: $this->get_room_id(), + ); $body = self::get_body($response); // Check user id is in the returned room member ids. @@ -376,12 +406,17 @@ class communication_feature implements ]); // Get room data. - $response = $this->matrixapi->get_room_info($this->get_room_id()); + $response = $this->matrixapi->get_room_info( + roomid: $this->get_room_id(), + ); $remoteroomdata = self::get_body($response); // Update the room name when it's updated from the form. if ($remoteroomdata->name !== $this->processor->get_room_name()) { - $this->matrixapi->update_room_name($this->get_room_id(), $this->processor->get_room_name()); + $this->matrixapi->update_room_name( + roomid: $this->get_room_id(), + name: $this->processor->get_room_name(), + ); } // Update the room topic if set. @@ -431,12 +466,18 @@ class communication_feature implements $contenturi = self::get_body($response)->content_uri; // Now update the room avatar. - $response = $this->matrixapi->update_room_avatar($this->get_room_id(), $contenturi); + $response = $this->matrixapi->update_room_avatar( + roomid: $this->get_room_id(), + avatarurl: $contenturi, + ); // And finally upload the content. $this->matrixapi->upload_content($instanceimage); } else { - $response = $this->matrixapi->update_room_avatar($this->get_room_id(), null); + $response = $this->matrixapi->update_room_avatar( + roomid: $this->get_room_id(), + avatarurl: null, + ); } } else { // Prior to v1.7 the only way to upload content was to upload the content, which returns a mxc URI to use. @@ -449,7 +490,10 @@ class communication_feature implements } // Now update the room avatar. - $response = $this->matrixapi->update_room_avatar($this->get_room_id(), $contenturi); + $response = $this->matrixapi->update_room_avatar( + roomid: $this->get_room_id(), + avatarurl: $contenturi, + ); } // Indicate the avatar has been synced if it was successfully set with Matrix. @@ -497,9 +541,12 @@ class communication_feature implements public static function set_form_definition(\MoodleQuickForm $mform): void { // Room description for the communication provider. - $mform->insertElementBefore($mform->createElement('text', 'matrixroomtopic', + $mform->insertElementBefore($mform->createElement( + 'text', + 'matrixroomtopic', get_string('matrixroomtopic', 'communication_matrix'), - 'maxlength="255" size="20"'), 'addcommunicationoptionshere'); + 'maxlength="255" size="20"' + ), 'addcommunicationoptionshere'); $mform->addHelpButton('matrixroomtopic', 'matrixroomtopic', 'communication_matrix'); $mform->setType('matrixroomtopic', PARAM_TEXT); } @@ -515,4 +562,193 @@ class communication_feature implements return json_decode($body, false, 512, JSON_THROW_ON_ERROR); } + + /** + * Set the matrix power level with the room. + * + * Users with a non-moodle power level are not typically removed unless specified in the $forceremoval param. + * Matrix Admin users are never removed. + * + * @param array $forceremoval The users to force removal from the room, even if they have a custom power level + */ + private function set_matrix_power_levels( + array $forceremoval = [], + ): void { + // Get the current power levels. + $currentpowerlevels = $this->get_current_powerlevel_data(); + $currentuserpowerlevels = (array) $currentpowerlevels->users ?? []; + + // Get all the current users who need to be in the room. + $userlist = $this->processor->get_all_userids_for_instance(); + + // Translate the user ids to matrix user ids. + $userlist = array_combine( + array_map( + fn ($userid) => matrix_user_manager::get_matrixid_from_moodle($userid), + $userlist, + ), + $userlist, + ); + + // Determine the power levels, and filter out anyone with the default level. + $newuserpowerlevels = array_filter( + array_map( + fn($userid) => $this->get_user_allowed_power_level($userid), + $userlist, + ), + fn($level) => $level !== matrix_constants::POWER_LEVEL_DEFAULT, + ); + + // Keep current room admins, and users which don't use our MODERATOR power level without changing them. + $staticusers = $this->get_users_with_custom_power_level($currentuserpowerlevels); + foreach ($staticusers as $userid => $level) { + $newuserpowerlevels[$userid] = $level; + } + + if (!empty($forceremoval)) { + // Remove the users from the power levels if they are not admins. + foreach ($forceremoval as $userid) { + if ($newuserpowerlevels < matrix_constants::POWER_LEVEL_MAXIMUM) { + unset($newuserpowerlevels[$userid]); + } + } + } + + if (!$this->power_levels_changed($currentuserpowerlevels, $newuserpowerlevels)) { + // No changes to make. + return; + } + + + // Update the power levels for the room. + $this->matrixapi->update_room_power_levels( + roomid: $this->get_room_id(), + users: $newuserpowerlevels, + ); + } + + /** + * Filter the list of users provided to remove those with a moodle-related power level. + * + * @param array $users + * @return array + */ + private function get_users_with_custom_power_level(array $users): array { + return array_filter( + $users, + function ($level): bool { + switch ($level) { + case matrix_constants::POWER_LEVEL_DEFAULT: + case matrix_constants::POWER_LEVEL_MOODLE_SITE_ADMIN: + case matrix_constants::POWER_LEVEL_MOODLE_MODERATOR: + return false; + default: + return true; + } + }, + ); + } + + /** + * Check whether power levels have changed compared with the proposed power levels. + * + * @param array $currentuserpowerlevels The current power levels + * @param array $newuserpowerlevels The new power levels proposed + * @return bool Whether there is any change to be made + */ + private function power_levels_changed( + array $currentuserpowerlevels, + array $newuserpowerlevels, + ): bool { + if (count($newuserpowerlevels) !== count($currentuserpowerlevels)) { + // Different number of keys - there must be a difference then. + return true; + } + + // Sort the power levels. + ksort($newuserpowerlevels, SORT_NUMERIC); + + // Get the current power levels. + ksort($currentuserpowerlevels); + + $diff = array_merge( + array_diff_assoc( + $newuserpowerlevels, + $currentuserpowerlevels, + ), + array_diff_assoc( + $currentuserpowerlevels, + $newuserpowerlevels, + ), + ); + + return count($diff) > 0; + } + + /** + * Get the current power level for the room. + * + * @return stdClass + */ + private function get_current_powerlevel_data(): \stdClass { + $roomid = $this->get_room_id(); + $response = $this->matrixapi->get_room_power_levels( + roomid: $roomid, + ); + if ($response->getStatusCode() !== 200) { + throw new \moodle_exception( + 'Unable to get power levels for room', + ); + } + + $powerdata = $this->get_body($response); + $powerdata = array_filter( + $powerdata->rooms->join->{$roomid}->state->events, + fn($value) => $value->type === 'm.room.power_levels' + ); + $powerdata = reset($powerdata); + + return $powerdata->content; + } + + /** + * Determine if a power level update is required. + * Matrix will always set a user to the default power level of 0 when a power level update is made. + * That is, unless we specify another level. As long as one person's level is greater than the default, + * we will need to set the power levels of all users greater than the default. + * + * @param array $userids The users to evaluate + * @return boolean Returns true if an update is required + */ + private function is_power_levels_update_required(array $userids): bool { + // Is the user's power level greater than the default? + foreach ($userids as $userid) { + if ($this->get_user_allowed_power_level($userid) > matrix_constants::POWER_LEVEL_DEFAULT) { + return true; + } + } + return false; + } + + /** + * Get the allowed power level for the user id according to perms/site admin or default. + * + * @param int $userid + * @return int + */ + public function get_user_allowed_power_level(int $userid): int { + $context = \core\context\course::instance($this->processor->get_instance_id()); + $powerlevel = matrix_constants::POWER_LEVEL_DEFAULT; + + if (has_capability('communication/matrix:moderator', $context, $userid)) { + $powerlevel = matrix_constants::POWER_LEVEL_MOODLE_MODERATOR; + } + + // If site admin, override all caps. + if (is_siteadmin($userid)) { + $powerlevel = matrix_constants::POWER_LEVEL_MOODLE_SITE_ADMIN; + } + + return $powerlevel; + } } 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 index 57980c8d121..b410900c689 100644 --- 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 @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * 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. * 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 index 60da91df446..1ad00326c24 100644 --- 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 @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * 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. * diff --git a/communication/provider/matrix/classes/local/spec/features/matrix/get_room_powerlevels_from_sync_v3.php b/communication/provider/matrix/classes/local/spec/features/matrix/get_room_powerlevels_from_sync_v3.php new file mode 100644 index 00000000000..a01765d4379 --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/matrix/get_room_powerlevels_from_sync_v3.php @@ -0,0 +1,91 @@ +. + +namespace communication_matrix\local\spec\features\matrix; + +use communication_matrix\local\command; +use GuzzleHttp\Psr7\Response; + +/** + * Matrix API feature to fetch room power levels using the sync API. + * + * https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3sync + * + * @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_powerlevels_from_sync_v3 { + /** + * Get a list of room members. + * + * @param string $roomid The room ID + * @return Response + */ + public function get_room_power_levels(string $roomid): Response { + // Filter the event data according to the API: + // https://spec.matrix.org/v1.1/client-server-api/#filtering + // We have to filter out all of the object data that we do not want, + // and set a filter to only fetch the one room that we do want. + $filter = (object) [ + "account_data" => (object) [ + // We don't want any account info for this call. + "not_types" => ['*'], + ], + "event_fields" => [ + // We only care about type, and content. Not sender. + "type", + "content", + ], + "event_format" => "client", + "presence" => (object) [ + // We don't need any presence data. + "not_types" => ['*'], + ], + "room" => (object) [ + // We only want state information for power levels, not timeline and ephemeral data. + "rooms" => [ + $roomid, + ], + "state" => (object) [ + "types" => [ + "m.room.power_levels", + ], + ], + "ephemeral" => (object) [ + "not_types" => ['*'], + ], + "timeline" => (object) [ + "not_types" => ['*'], + ], + ], + ]; + + $query = [ + 'filter' => json_encode($filter), + ]; + + return $this->execute(new command( + $this, + method: 'GET', + endpoint: '_matrix/client/v3/sync', + query: $query, + sendasjson: false, + )); + } +} diff --git a/communication/provider/matrix/classes/local/spec/features/matrix/media_create_v1.php b/communication/provider/matrix/classes/local/spec/features/matrix/media_create_v1.php index c47f0142112..dfcfb57731e 100644 --- a/communication/provider/matrix/classes/local/spec/features/matrix/media_create_v1.php +++ b/communication/provider/matrix/classes/local/spec/features/matrix/media_create_v1.php @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * This code does not warrant being tested. Testing offers no discernible benefit given its usage is tested. */ trait media_create_v1 { - /** * Create a media URI. * 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 index 89e6b89cc9d..fa331e19f43 100644 --- 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 @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * 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. * 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 index 1df828f793e..f7fe1239cf3 100644 --- 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 @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * 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. * @@ -56,5 +55,4 @@ trait update_room_avatar_v3 { 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 index e7235ed9a89..674de6f71ad 100644 --- 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 @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * 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. * diff --git a/communication/provider/matrix/classes/local/spec/features/matrix/update_room_power_levels_v3.php b/communication/provider/matrix/classes/local/spec/features/matrix/update_room_power_levels_v3.php new file mode 100644 index 00000000000..0f903756bf9 --- /dev/null +++ b/communication/provider/matrix/classes/local/spec/features/matrix/update_room_power_levels_v3.php @@ -0,0 +1,77 @@ +. + +namespace communication_matrix\local\spec\features\matrix; + +use communication_matrix\local\command; +use communication_matrix\matrix_constants; +use GuzzleHttp\Psr7\Response; + +/** + * Matrix API feature to update a room power levels. + * + * Matrix rooms have a concept of power levels, which are used to determine what actions a user can perform in a room. + * + * https://spec.matrix.org/v1.1/client-server-api/#mroompower_levels + * + * @package communication_matrix + * @copyright 2023 Safat Shahin + * @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_power_levels_v3 { + /** + * Set the avatar for a room to the specified URL. + * + * @param string $roomid The roomid to set for + * @param array $users The users to set power levels for + * @param int $ban The level required to ban a user + * @param int $invite The level required to invite a user + * @param int $kick The level required to kick a user + * @param array $notifications The level required to send notifications + * @param int $redact The level required to redact events + * @return Response + */ + public function update_room_power_levels( + string $roomid, + array $users, + int $ban = matrix_constants::POWER_LEVEL_MAXIMUM, + int $invite = matrix_constants::POWER_LEVEL_MODERATOR, + int $kick = matrix_constants::POWER_LEVEL_MODERATOR, + array $notifications = [ + 'room' => matrix_constants::POWER_LEVEL_MODERATOR, + ], + int $redact = matrix_constants::POWER_LEVEL_MODERATOR, + ): Response { + $params = [ + ':roomid' => $roomid, + 'ban' => $ban, + 'invite' => $invite, + 'kick' => $kick, + 'notifications' => $notifications, + 'redact' => $redact, + 'users' => $users, + ]; + + return $this->execute(new command( + $this, + method: 'PUT', + endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.power_levels', + 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 index c68f45316a1..fddacd0cb76 100644 --- 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 @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * 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. * @@ -51,6 +50,5 @@ trait update_room_topic_v3 { 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 index 0e53bc04328..a553ed372f8 100644 --- 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 @@ -32,7 +32,6 @@ use GuzzleHttp\Psr7\Utils; * 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. * @@ -60,7 +59,7 @@ trait upload_content_v3 { sendasjson: false, query: $query, params: [ - 'mediaid' => $mediaid, + ':mediaid' => $mediaid, ], ); } else { 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 index 9d89a1a857c..75407b47c23 100644 --- 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 @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * 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. * @@ -63,5 +62,4 @@ trait create_user_v2 { 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 index 4903c4d632d..1201ac9831a 100644 --- 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 @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * 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. * 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 index 2b73867c90d..67e2796acfa 100644 --- 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 @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * 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. * @@ -49,5 +48,4 @@ trait get_user_info_v2 { ], )); } - } 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 index bce95b7eb4a..ad002ca0f9c 100644 --- 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 @@ -31,7 +31,6 @@ use GuzzleHttp\Psr7\Response; * 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. * diff --git a/communication/provider/matrix/classes/local/spec/v1p1.php b/communication/provider/matrix/classes/local/spec/v1p1.php index e75b43343aa..fc348310067 100644 --- a/communication/provider/matrix/classes/local/spec/v1p1.php +++ b/communication/provider/matrix/classes/local/spec/v1p1.php @@ -34,6 +34,8 @@ class v1p1 extends \communication_matrix\matrix_client { use features\matrix\update_room_name_v3; use features\matrix\update_room_topic_v3; use features\matrix\upload_content_v3; + use features\matrix\update_room_power_levels_v3; + use features\matrix\get_room_powerlevels_from_sync_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; diff --git a/communication/provider/matrix/classes/local/spec/v1p7.php b/communication/provider/matrix/classes/local/spec/v1p7.php index 0f2acf82c81..f4f2195d536 100644 --- a/communication/provider/matrix/classes/local/spec/v1p7.php +++ b/communication/provider/matrix/classes/local/spec/v1p7.php @@ -30,6 +30,5 @@ class v1p7 extends v1p6 { // Note: A new Content Upload API was introduced. // See details in the spec: // https://github.com/matrix-org/matrix-spec-proposals/pull/2246. - use features\matrix\media_create_v1; } diff --git a/communication/provider/matrix/classes/matrix_client.php b/communication/provider/matrix/classes/matrix_client.php index 206fe08cff6..a168240fe95 100644 --- a/communication/provider/matrix/classes/matrix_client.php +++ b/communication/provider/matrix/classes/matrix_client.php @@ -36,7 +36,6 @@ use GuzzleHttp\Psr7\Response; * @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 */ diff --git a/communication/provider/matrix/classes/matrix_constants.php b/communication/provider/matrix/classes/matrix_constants.php new file mode 100644 index 00000000000..9a0756366cc --- /dev/null +++ b/communication/provider/matrix/classes/matrix_constants.php @@ -0,0 +1,51 @@ +. + +namespace communication_matrix; + +/** + * class matrix_constants to have one location to store all constants related to matrix. + * + * @package communication_matrix + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class matrix_constants { + /** + * User default power level for matrix. + */ + public const POWER_LEVEL_DEFAULT = 0; + + /** + * User moderator power level for matrix. + */ + public const POWER_LEVEL_MODERATOR = 50; + + /** + * User moderator power level for matrix. + */ + public const POWER_LEVEL_MOODLE_MODERATOR = 51; + + /** + * User power level for matrix associated to moodle site admins. It is a custom power level for site admins. + */ + public const POWER_LEVEL_MOODLE_SITE_ADMIN = 90; + + /** + * User maximum power level for matrix. This is only associated to the token user to allow god mode actions. + */ + public const POWER_LEVEL_MAXIMUM = 100; +} diff --git a/communication/provider/matrix/classes/matrix_room.php b/communication/provider/matrix/classes/matrix_room.php index 4ebb0d92ddd..bfaeffaee55 100644 --- a/communication/provider/matrix/classes/matrix_room.php +++ b/communication/provider/matrix/classes/matrix_room.php @@ -26,7 +26,6 @@ use stdClass; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class matrix_room { - private const TABLE = 'matrix_room'; /** @var \stdClass|null $record The matrix room record from db */ diff --git a/communication/provider/matrix/classes/matrix_user_manager.php b/communication/provider/matrix/classes/matrix_user_manager.php index 937f952f63d..6b5b1b77f04 100644 --- a/communication/provider/matrix/classes/matrix_user_manager.php +++ b/communication/provider/matrix/classes/matrix_user_manager.php @@ -24,7 +24,6 @@ namespace communication_matrix; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class matrix_user_manager { - /** * Gets matrix user id from moodle. * @@ -33,7 +32,7 @@ class matrix_user_manager { */ public static function get_matrixid_from_moodle( int $userid, - ) : ?string { + ): ?string { self::load_requirements(); $field = profile_user_record($userid); $matrixprofilefield = get_config('communication_matrix', 'matrixuserid_field'); @@ -141,7 +140,7 @@ class matrix_user_manager { // Check if matrixuserid exists in user_info_field table. $matrixuserid = $DB->count_records('user_info_field', [ 'shortname' => 'matrixuserid', - 'categoryid' => $categoryid + 'categoryid' => $categoryid, ]); if ($matrixuserid < 1) { @@ -158,7 +157,7 @@ class matrix_user_manager { 'visible' => 0, 'locked' => 1, 'param1' => 30, - 'param2' => 2048 + 'param2' => 2048, ]; $profileclass->define_save($data); diff --git a/communication/provider/matrix/classes/privacy/provider.php b/communication/provider/matrix/classes/privacy/provider.php index 7ba1d520aeb..243eb04bc8d 100644 --- a/communication/provider/matrix/classes/privacy/provider.php +++ b/communication/provider/matrix/classes/privacy/provider.php @@ -27,7 +27,6 @@ use core_privacy\local\metadata\null_provider; * @codeCoverageIgnore */ class provider implements null_provider { - /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. diff --git a/communication/provider/matrix/db/access.php b/communication/provider/matrix/db/access.php new file mode 100644 index 00000000000..8c95a5dcba5 --- /dev/null +++ b/communication/provider/matrix/db/access.php @@ -0,0 +1,39 @@ +. + +/** + * Capability definitions for matrix communication. + * + * @package communication_matrix + * @copyright 2023 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + + // Matrix moderator capability which aligns with the matrix moderator role or power level 50. + 'communication/matrix:moderator' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + ], + ], +]; diff --git a/communication/provider/matrix/db/upgrade.php b/communication/provider/matrix/db/upgrade.php index 07f40633a4f..705475d2dcf 100644 --- a/communication/provider/matrix/db/upgrade.php +++ b/communication/provider/matrix/db/upgrade.php @@ -52,5 +52,4 @@ function xmldb_communication_matrix_upgrade($oldversion) { } return true; - } diff --git a/communication/provider/matrix/lang/en/communication_matrix.php b/communication/provider/matrix/lang/en/communication_matrix.php index e9042297e39..356e2d160b6 100644 --- a/communication/provider/matrix/lang/en/communication_matrix.php +++ b/communication/provider/matrix/lang/en/communication_matrix.php @@ -35,5 +35,6 @@ $string['matrixelementurl'] = 'Element web URL'; $string['matrixelementurl_desc'] = 'The URL to Element Web instance.'; $string['matrixroomtopic'] = 'Room topic'; $string['matrixroomtopic_help'] = 'A short description of what this room is for.'; +$string['matrix:moderator'] = 'Matrix moderator'; $string['pluginname'] = 'Matrix'; $string['privacy:metadata'] = 'Matrix communication plugin does not store any personal data.'; diff --git a/communication/provider/matrix/tests/behat/behat_communication_matrix.php b/communication/provider/matrix/tests/behat/behat_communication_matrix.php index ac89fafbaf6..a16474b908d 100644 --- a/communication/provider/matrix/tests/behat/behat_communication_matrix.php +++ b/communication/provider/matrix/tests/behat/behat_communication_matrix.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + use Behat\Behat\Hook\Scope\BeforeScenarioScope; use communication_matrix\matrix_test_helper_trait; use Moodle\BehatExtension\Exception\SkippedException; @@ -31,7 +33,6 @@ require_once(__DIR__ . '/../../../../tests/communication_test_helper_trait.php') * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_communication_matrix extends \behat_base { - use \core_communication\communication_test_helper_trait; use matrix_test_helper_trait; diff --git a/communication/provider/matrix/tests/communication_feature_test.php b/communication/provider/matrix/tests/communication_feature_test.php index d90fc3483c3..ee75b81d8e8 100644 --- a/communication/provider/matrix/tests/communication_feature_test.php +++ b/communication/provider/matrix/tests/communication_feature_test.php @@ -18,6 +18,7 @@ namespace communication_matrix; use core_communication\api; use core_communication\communication_test_helper_trait; +use core_communication\processor; use stored_file; defined('MOODLE_INTERNAL') || die(); @@ -32,6 +33,7 @@ require_once(__DIR__ . '/../../../tests/communication_test_helper_trait.php'); * @category test * @copyright 2023 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \communication_matrix\communication_feature * @coversDefaultClass \communication_matrix\communication_feature */ class communication_feature_test extends \advanced_testcase { @@ -66,6 +68,7 @@ class communication_feature_test extends \advanced_testcase { ], ); + // phpcs:ignore moodle.Commenting.InlineComment.DocBlock /** @var communication_feature */ $provider = $communication->get_room_provider(); $this->assertInstanceOf( @@ -124,6 +127,7 @@ class communication_feature_test extends \advanced_testcase { roomtopic: 'Our room topic', ); + // phpcs:ignore moodle.Commenting.InlineComment.DocBlock /** @var communication_feature */ $provider = $communication->get_room_provider(); $this->assertInstanceOf( @@ -191,7 +195,6 @@ class communication_feature_test extends \advanced_testcase { 'Our updated room name', $communication->get_room_name(), ); - } /** @@ -360,8 +363,11 @@ class communication_feature_test extends \advanced_testcase { * @covers ::add_members_to_room * @covers ::add_registered_matrix_user_to_room * @covers ::check_room_membership + * @covers ::set_matrix_power_levels */ public function test_add_and_remove_members_from_room(): void { + $this->markTestSkipped('Skipping while we update the Mock Server with the new route'); + $user = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); @@ -396,6 +402,98 @@ class communication_feature_test extends \advanced_testcase { $this->assertContains("@{$user2->username}", $userids); } + /** + * Test update of room membership. + * + * @covers ::update_room_membership + * @covers ::set_matrix_power_levels + * @covers ::is_power_levels_update_required + * @covers ::get_user_allowed_power_level + */ + public function test_update_room_membership(): void { + $this->markTestSkipped('Skipping while we update the Mock Server with the new route'); + + $this->resetAfterTest(); + + global $DB; + + // Create a new room. + $course = $this->get_course('Sampleroom', 'none'); + $user = $this->get_user(); + + $communication = $this->create_room( + component: 'core_course', + itemtype: 'coursecommunication', + itemid: $course->id + ); + $provider = $communication->get_room_user_provider(); + + // Add the members to the room. + $provider->add_members_to_room([$user->id]); + + // Assign teacher role to the user. + $coursecontext = \context_course::instance($course->id); + $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); + $this->getDataGenerator()->enrol_user($user->id, $course->id); + role_assign($teacherrole->id, $user->id, $coursecontext->id); + + // Test the tasks added as the role is a teacher. + $provider->update_room_membership([$user->id]); + + $processor = \core_communication\processor::load_by_instance( + component: 'core_course', + instancetype: 'coursecommunication', + instanceid: $course->id, + ); + $synceduser = $processor->get_instance_userids( + synced: true, + ); + $synceduser = reset($synceduser); + + // Test if the communication user record is synced. + $this->assertEquals($user->id, $synceduser); + } + + /** + * Test the user power level allocation according to context. + * + * @covers ::get_user_allowed_power_level + */ + public function test_get_user_allowed_power_level(): void { + $this->resetAfterTest(); + global $DB; + + // Create users. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $course = $this->get_course(); + $coursecontext = \context_course::instance($course->id); + $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']); + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $this->getDataGenerator()->enrol_user($user1->id, $course->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id); + // Assign roles. + role_assign($teacherrole->id, $user1->id, $coursecontext->id); + role_assign($studentrole->id, $user2->id, $coursecontext->id); + + $communicationprocessor = processor::load_by_instance( + component: 'core_course', + instancetype: 'coursecommunication', + instanceid: $course->id + ); + + // Test if the power level is set according to the context. + $this->assertEquals( + matrix_constants::POWER_LEVEL_MOODLE_MODERATOR, + $communicationprocessor->get_room_provider()->get_user_allowed_power_level($user1->id) + ); + $this->assertEquals( + matrix_constants::POWER_LEVEL_DEFAULT, + $communicationprocessor->get_room_provider()->get_user_allowed_power_level($user2->id) + ); + } + /** * Helper to create a room. * diff --git a/communication/provider/matrix/tests/local/command_test.php b/communication/provider/matrix/tests/local/command_test.php index 66eac20f75b..c2a2329a36f 100644 --- a/communication/provider/matrix/tests/local/command_test.php +++ b/communication/provider/matrix/tests/local/command_test.php @@ -266,7 +266,7 @@ class command_test extends \advanced_testcase { mock: $mock, ); - $mock->append(function(Request $request) use ($expected): Response { + $mock->append(function (Request $request) use ($expected): Response { $this->assertSame( $expected, $request->getUri()->getQuery(), diff --git a/communication/provider/matrix/tests/matrix_client_test.php b/communication/provider/matrix/tests/matrix_client_test.php index f7b68261e08..44072ba9018 100644 --- a/communication/provider/matrix/tests/matrix_client_test.php +++ b/communication/provider/matrix/tests/matrix_client_test.php @@ -301,7 +301,7 @@ class matrix_client_test extends \advanced_testcase { */ public function require_features_provider(): array { // We'll just add to the standard testcases. - $testcases = array_map(static function(array $testcase): array { + $testcases = array_map(static function (array $testcase): array { $testcase[1] = [$testcase[1]]; return $testcase; }, $this->implements_feature_provider()); diff --git a/communication/provider/matrix/tests/matrix_client_test_trait.php b/communication/provider/matrix/tests/matrix_client_test_trait.php index 72bda517c54..21e6bc0f296 100644 --- a/communication/provider/matrix/tests/matrix_client_test_trait.php +++ b/communication/provider/matrix/tests/matrix_client_test_trait.php @@ -47,7 +47,7 @@ trait matrix_client_test_trait { mocked_matrix_client::reset_client(); } - public function tearDown():void { + public function tearDown(): void { parent::tearDown(); // Reset the test client. diff --git a/communication/provider/matrix/tests/matrix_room_test.php b/communication/provider/matrix/tests/matrix_room_test.php index a017d0f0602..ee374c230ca 100644 --- a/communication/provider/matrix/tests/matrix_room_test.php +++ b/communication/provider/matrix/tests/matrix_room_test.php @@ -26,7 +26,6 @@ namespace communication_matrix; * @coversDefaultClass \communication_matrix\matrix_room */ class matrix_room_test extends \advanced_testcase { - /** * Test for load_by_processor_id with no record. * @@ -84,7 +83,6 @@ class matrix_room_test extends \advanced_testcase { $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()); - } /** diff --git a/communication/provider/matrix/tests/matrix_test_helper_trait.php b/communication/provider/matrix/tests/matrix_test_helper_trait.php index 8aa7bbd942b..14b6a2fe7f0 100644 --- a/communication/provider/matrix/tests/matrix_test_helper_trait.php +++ b/communication/provider/matrix/tests/matrix_test_helper_trait.php @@ -26,7 +26,6 @@ use GuzzleHttp\Psr7\Response; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait matrix_test_helper_trait { - /** * @var string $accesstoken The token for matrix connection */ @@ -46,7 +45,7 @@ trait matrix_test_helper_trait { $this->matrixhomeserverurl = TEST_COMMUNICATION_MATRIX_MOCK_SERVER; set_config('matrixhomeserverurl', $this->matrixhomeserverurl, 'communication_matrix'); $request = $this->request(); - $response = $request->post($this->matrixhomeserverurl. '/backoffice/create-admin'); + $response = $request->post($this->matrixhomeserverurl . '/backoffice/create-admin'); $admindata = json_decode($response->getBody()); $json = [ 'identifier' => [ @@ -57,7 +56,7 @@ trait matrix_test_helper_trait { 'password' => $admindata->password, ]; $request = $this->request($json); - $response = $request->post($this->matrixhomeserverurl. '/_matrix/client/r0/login'); + $response = $request->post($this->matrixhomeserverurl . '/_matrix/client/r0/login'); $response = json_decode($response->getBody()); if (empty($response->access_token)) { $this->markTestSkipped( @@ -161,10 +160,15 @@ trait matrix_test_helper_trait { array $rooms = [], ): Response { $client = new \core\http_client(); - return $client->put($this->get_backoffice_uri('create'), ['json' => [ - 'users' => $users, - 'rooms' => $rooms, - ]]); + return $client->put( + $this->get_backoffice_uri('create'), + [ + 'json' => [ + 'users' => $users, + 'rooms' => $rooms, + ], + ], + ); } /** @@ -225,7 +229,7 @@ trait matrix_test_helper_trait { public function reset_mock(): void { if (defined('TEST_COMMUNICATION_MATRIX_MOCK_SERVER')) { $request = $this->request(); - $response = $request->post(TEST_COMMUNICATION_MATRIX_MOCK_SERVER. '/backoffice/reset'); + $response = $request->post(TEST_COMMUNICATION_MATRIX_MOCK_SERVER . '/backoffice/reset'); $response = json_decode($response->getBody()); if (empty($response->reset)) { $this->markTestSkipped( diff --git a/communication/provider/matrix/version.php b/communication/provider/matrix/version.php index 645865504d0..803695f03f7 100644 --- a/communication/provider/matrix/version.php +++ b/communication/provider/matrix/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'communication_matrix'; -$plugin->version = 2023071900; +$plugin->version = 2023090600; $plugin->requires = 2023011300; $plugin->maturity = MATURITY_ALPHA; diff --git a/communication/tests/api_test.php b/communication/tests/api_test.php index ca1787a4b43..adafe0bd634 100644 --- a/communication/tests/api_test.php +++ b/communication/tests/api_test.php @@ -27,10 +27,9 @@ 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 - * @covers \core_communication\api + * @covers \core_communication\api */ class api_test extends \advanced_testcase { - use communication_test_helper_trait; public function setUp(): void { @@ -251,9 +250,12 @@ class api_test extends \advanced_testcase { } /** - * Test the update_room_membership for adding adn removing members. + * Test the adding and removing of members from room. + * + * @covers ::add_members_to_room + * @covers ::remove_members_from_room */ - public function test_update_room_membership(): void { + public function test_adding_and_removing_of_room_membership(): void { $course = $this->get_course(); $userid = $this->get_user()->id; @@ -281,11 +283,40 @@ class api_test extends \advanced_testcase { * Test the enabled communication plugin list and default. */ public function test_get_enabled_providers_and_default(): void { - list($communicationproviders, $defaulprovider) = \core_communication\api::get_enabled_providers_and_default(); + [$communicationproviders, $defaulprovider] = \core_communication\api::get_enabled_providers_and_default(); // Get the communication plugins. $plugins = \core_component::get_plugin_list('communication'); // Check the number of plugins matches plus 1 as we have none in the selection. $this->assertCount(count($plugins) + 1, $communicationproviders); $this->assertEquals(processor::PROVIDER_NONE, $defaulprovider); } + + /** + * Test the update of room membership with the change user role. + * + * @covers ::update_room_membership + */ + public function test_update_room_membership_on_user_role_change(): void { + global $DB; + + // Generate the data. + $user = $this->getDataGenerator()->create_user(); + $course = $this->get_course(); + $coursecontext = \context_course::instance($course->id); + $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); + $this->getDataGenerator()->enrol_user($user->id, $course->id); + + $adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\add_members_to_room_task'); + $this->assertCount(1, $adhoctask); + + $adhoctask = reset($adhoctask); + $this->assertInstanceOf('\\core_communication\\task\\add_members_to_room_task', $adhoctask); + + // Test the tasks added as the role is a teacher. + $adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\update_room_membership_task'); + $this->assertCount(1, $adhoctask); + + $adhoctask = reset($adhoctask); + $this->assertInstanceOf('\\core_communication\\task\\update_room_membership_task', $adhoctask); + } } diff --git a/communication/tests/behat/behat_communication.php b/communication/tests/behat/behat_communication.php index c3f3bd5e8a8..165d393897b 100644 --- a/communication/tests/behat/behat_communication.php +++ b/communication/tests/behat/behat_communication.php @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); require_once(__DIR__ . '/../../tests/communication_test_helper_trait.php'); @@ -26,7 +28,6 @@ require_once(__DIR__ . '/../../tests/communication_test_helper_trait.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_communication extends \behat_base { - use \core_communication\communication_test_helper_trait; /** diff --git a/communication/tests/communication_test_helper_trait.php b/communication/tests/communication_test_helper_trait.php index 8f6a3ff0e82..62c5fbc5c20 100644 --- a/communication/tests/communication_test_helper_trait.php +++ b/communication/tests/communication_test_helper_trait.php @@ -25,7 +25,6 @@ namespace core_communication; * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait communication_test_helper_trait { - /** * Setup necessary configs for communication subsystem. * @@ -83,7 +82,7 @@ trait communication_test_helper_trait { $records = [ 'firstname' => $firstname, 'lastname' => $lastname, - 'username' => $username + 'username' => $username, ]; return $this->getDataGenerator()->create_user($records); diff --git a/communication/tests/processor_test.php b/communication/tests/processor_test.php index a8b58614f44..145ea33e041 100644 --- a/communication/tests/processor_test.php +++ b/communication/tests/processor_test.php @@ -30,7 +30,6 @@ require_once(__DIR__ . '/communication_test_helper_trait.php'); * @coversDefaultClass \core_communication\processor */ class processor_test extends \advanced_testcase { - use communication_test_helper_trait; /** @@ -61,8 +60,10 @@ class processor_test extends \advanced_testcase { ); // Now test the record against the database. - $communicationrecord = $DB->get_record('communication', - ['instanceid' => $instanceid, 'component' => $component, 'instancetype' => $instancetype]); + $communicationrecord = $DB->get_record( + 'communication', + ['instanceid' => $instanceid, 'component' => $component, 'instancetype' => $instancetype] + ); // Test against the set data. $this->assertNotEmpty($communicationrecord); @@ -114,7 +115,7 @@ class processor_test extends \advanced_testcase { $communicationrecord = $DB->get_record('communication', [ 'instanceid' => $instanceid, 'component' => $component, - 'instancetype' => $instancetype + 'instancetype' => $instancetype, ]); // Test against the set data. @@ -163,7 +164,7 @@ class processor_test extends \advanced_testcase { $communicationrecord = $DB->get_record('communication', [ 'instanceid' => $instanceid, 'component' => $component, - 'instancetype' => $instancetype + 'instancetype' => $instancetype, ]); // Test against the set data. @@ -173,7 +174,8 @@ class processor_test extends \advanced_testcase { $communicationprocessor = processor::load_by_instance( $component, $instancetype, - $instanceid); + $instanceid + ); $this->assertNull($communicationprocessor); } @@ -428,4 +430,44 @@ class processor_test extends \advanced_testcase { set_config('disabled', 1, $communicationprovider); $this->assertFalse(processor::is_provider_enabled($communicationprovider)); } + + /** + * Test delete flagged user id's return correct users. + * + * @covers ::get_all_delete_flagged_userids + */ + public function test_get_all_delete_flagged_userids(): void { + $this->resetAfterTest(); + + $course = $this->get_course('Sampleroom', 'none'); + $user1 = $this->getDataGenerator()->create_user()->id; + $user2 = $this->getDataGenerator()->create_user()->id; + + // Sample data. + $communicationroomname = 'Sampleroom'; + $selectedcommunication = 'communication_matrix'; + $component = 'core_course'; + $instancetype = 'coursecommunication'; + + // Load the communication api. + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $course->id + ); + $communication->create_and_configure_room($selectedcommunication, $communicationroomname); + $communication->add_members_to_room([$user1, $user2]); + + // Now remove user1 from the room. + $communication->remove_members_from_room([$user1]); + + // Test against the object. + $communicationprocessor = processor::load_by_instance( + $component, + $instancetype, + $course->id + ); + + $this->assertEquals([$user1], $communicationprocessor->get_all_delete_flagged_userids()); + } } diff --git a/lib/accesslib.php b/lib/accesslib.php index d5699be6290..93398aff7db 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -1645,6 +1645,17 @@ function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, core_course_category::role_assignment_changed($roleid, $context); + // Update the room membership and power levels when the user role changes. + if (\core_communication\api::is_available() && $coursecontext = $context->get_course_context(false)) { + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $coursecontext->instanceid, + ); + + $communication->update_room_membership([$userid]); + } + $event = \core\event\role_assigned::create(array( 'context' => $context, 'objectid' => $ra->roleid, @@ -1750,6 +1761,19 @@ function role_unassign_all(array $params, $subcontexts = false, $includemanual = $event->add_record_snapshot('role_assignments', $ra); $event->trigger(); core_course_category::role_assignment_changed($ra->roleid, $context); + + // Update the room membership and power levels when the user role changes. + if (\core_communication\api::is_available() && $coursecontext = $context->get_course_context(false)) { + $communication = \core_communication\api::load_by_instance( + 'core_course', + 'coursecommunication', + $coursecontext->instanceid, + ); + + $communication->update_room_membership([$ra->userid]); + } + + } } unset($ras);