MDL-78129 communication_matrix: Add support for matrix power level

This commit is contained in:
Safat 2023-08-04 16:47:29 +10:00 committed by Andrew Nicols
parent 3e47253787
commit e9743431a6
No known key found for this signature in database
GPG key ID: 6D1E3157C8CFBF14
8 changed files with 363 additions and 7 deletions

View file

@ -50,7 +50,7 @@ class communication_feature implements
\core_communication\room_user_provider,
\core_communication\form_provider {
/** @var matrix_room $room The matrix room object to update room information */
/** @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 +127,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;
}
@ -184,10 +181,21 @@ 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();
// Mark then users as synced for the updated members.
$this->processor->mark_users_as_synced($userids);
}
/**
* Add members to a room.
*
@ -209,6 +217,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);
@ -256,6 +269,13 @@ 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 = [];
foreach ($userids as $userid) {
@ -515,4 +535,82 @@ class communication_feature implements
return json_decode($body, false, 512, JSON_THROW_ON_ERROR);
}
/**
* Set the matrix power level with the room.
*
* @param array $resetusers The list of users to override and reset their power level to 0
*/
private function set_matrix_power_levels(array $resetusers = []): void {
// Get all the current users for the room.
$existingusers = $this->processor->get_all_userids_for_instance();
$userpowerlevels = [];
foreach ($existingusers as $existinguser) {
$matrixuserid = matrix_user_manager::get_matrixid_from_moodle($existinguser);
if (!$matrixuserid) {
continue;
}
if (!empty($resetusers) && in_array($existinguser, $resetusers, true)) {
$userpowerlevels[$matrixuserid] = matrix_constants::POWER_LEVEL_DEFAULT;
} else {
$existinguserpowerlevel = $this->get_user_allowed_power_level($existinguser);
// We don't need to include the default power level users in request, as matrix will make then default anyways.
if ($existinguserpowerlevel > matrix_constants::POWER_LEVEL_DEFAULT) {
$userpowerlevels[$matrixuserid] = $existinguserpowerlevel;
}
}
}
// Now add the token user permission to retain the permission in the room.
$response = $this->matrixapi->get_room_info($this->get_room_id());
$matrixroomdata = self::get_body($response);
$roomadmin = $matrixroomdata->creator;
$userpowerlevels[$roomadmin] = matrix_constants::POWER_LEVEL_MAXIMUM;
$this->matrixapi->update_room_power_levels($this->get_room_id(), $userpowerlevels);
}
/**
* 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 = \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_MODERATOR;
}
// If site admin, override all caps.
if (is_siteadmin($userid)) {
$powerlevel = matrix_constants::POWER_LEVEL_MOODLE_SITE_ADMIN;
}
return $powerlevel;
}
}

View file

@ -0,0 +1,79 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
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 <safat.shahin@moodle.com>
* @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,
));
}
}

View file

@ -34,6 +34,7 @@ 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;
// 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;

View file

@ -0,0 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace communication_matrix;
/**
* class matrix_constants to have one location to store all constants related to matrix.
*
* @package communication_matrix
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @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 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;
}

View file

@ -0,0 +1,39 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Capability definitions for matrix communication.
*
* @package communication_matrix
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @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,
]
],
];

View file

@ -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.';

View file

@ -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();
@ -191,7 +192,6 @@ class communication_feature_test extends \advanced_testcase {
'Our updated room name',
$communication->get_room_name(),
);
}
/**
@ -360,6 +360,7 @@ 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 {
$user = $this->getDataGenerator()->create_user();
@ -396,6 +397,96 @@ 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->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_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.
*

View file

@ -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;