Skip to content

Commit

Permalink
MDL-78129 communication_matrix: Simplify power level setting
Browse files Browse the repository at this point in the history
This change introduces a new API call to fetch the current power levels.

The result of this are used to fetch current admin users so as not to
remove them.

The update of existing users is simplified to only set users who do not
have the default level.
  • Loading branch information
andrewnicols committed Sep 22, 2023
1 parent eb99135 commit 01a3461
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 31 deletions.
146 changes: 115 additions & 31 deletions communication/provider/matrix/classes/communication_feature.php
Expand Up @@ -276,18 +276,27 @@ public function remove_members_from_room(array $userids): void {

$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(
Expand Down Expand Up @@ -554,39 +563,114 @@ public static function get_body(Response $response): stdClass {

/**
* Set the matrix power level with the room.
*/
private function set_matrix_power_levels(): 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 without changing them.
$currentadmins = array_filter(
$currentuserpowerlevels,
fn($level) => $level >= matrix_constants::POWER_LEVEL_MAXIMUM,
);
foreach ($currentadmins as $userid => $level) {
$newuserpowerlevels[$userid] = $level;
}

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

/**
* Check whether power levels have changed compared with the proposed power levels.
*
* @param array $resetusers The list of users to override and reset their power level to 0
* @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 set_matrix_power_levels(array $resetusers = []): void {
// Get all the current users for the room.
$existingusers = $this->processor->get_all_userids_for_instance();
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;
}

$userpowerlevels = [];
foreach ($existingusers as $existinguser) {
$matrixuserid = matrix_user_manager::get_matrixid_from_moodle($existinguser);
// 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,
),
);

if (!$matrixuserid) {
continue;
}
return count($diff) > 0;
}

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;
}
}
/**
* 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',
);
}

// 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;
$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);

$this->matrixapi->update_room_power_levels($this->get_room_id(), $userpowerlevels);
return $powerdata->content;
}

/**
Expand Down
@@ -0,0 +1,92 @@
<?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 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 <andrew@nicols.co.uk>
* @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,
));
}
}
1 change: 1 addition & 0 deletions communication/provider/matrix/classes/local/spec/v1p1.php
Expand Up @@ -35,6 +35,7 @@ class v1p1 extends \communication_matrix\matrix_client {
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;
Expand Down

0 comments on commit 01a3461

Please sign in to comment.