Files
moodle/communication/classes/processor.php
T
Andrew Nicols 914686bc5e MDL-77917 communication_matrix: Support server API versions
This commit brings in support for multiple versions of the Matrix
specification.

A Matrix server is compromised of a number of individually versioned API
endpoints, for example:

    /_matrix/client/v3/createRoom
    /_matrix/client/v3/rooms/:roomid/joined_members
    /_matrix/media/v1/create

The combination of a large number of these individually versioned
endpoints forms a Matrix Specification version.

For example:

* the /_matrix/media/v1/create endpoint was created for version 1.7 of the
  specification, and does not exist in earlier versions.
* in the future a new behaviour or parameter may be created for the
  `createRoom` endpoint and a new endpoint created at:

    /_matrix/client/v4/createRoom

A single server can support multiple versions of the Matrix
specification. The server declares the versions of the specification
that it supports using a non-versioned endpoint at
`/_matrix/client/versions`.

As a Matrix client, Moodle should:
* query the server version endpoint
* determine the combination of mutually supported Matrix specification
  versions
* create a client instance of the highest-supported version of the
  specification.

For example, if Moodle (Matrix client) and a remote server have the
following support:

```
Moodle:      1.1  1.2  1.3  1.4  1.5  1.6  1.7
Server:  r0  1.1  1.2  1.3  1.4  1.5  1.6
```

The versions in common are 1.1 through 1.6, and version 1.6 would be
chosen.

To avoid duplication and allow for support of future features more
easily, the Moodle client is written as:
* a set of classes named `v1p1` through `v1p7` (currently) which extend
  the `matrix_client` abstract class; and
* a set if PHP traits which provide the implementation for individual
  versioned endpoints.

Each client version then imports any relevant traits which are present
in that version of the Matrix Specification. For example versions 1.1 to
1.6 do _not_ have the `/_matrix/media/v1/create` endpoint so they do not
import this trait. This trait was introduced in version 1.7, so the
trait is included from that version onwards.

In the future, if an endpoint is created which conflicts with an
existing endpoint, then it would be easy to create a new client version
which uses the existing common traits, and adds the new trait.

Each endpoint is written using a `command` class which extends the
Guzzle implementation of the PSR-7 Request interface. This command
class adds support for easy creation of:
* path parameters within the URI
* query parameters
* body parameters

This is done to avoid complex patterns of Request creation which are
repeated for every client endpoint.
2023-08-24 11:59:25 +08:00

660 lines
18 KiB
PHP

<?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 core_communication;
use stdClass;
use stored_file;
/**
* Class processor to manage the base operations of the providers.
*
* This class is responsible for creating, updating, deleting and loading the communication instance, associated actions.
*
* @package core_communication
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @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';
/** @var int The provider active flag */
public const PROVIDER_ACTIVE = 1;
/** @var int The provider inactive flag */
public const PROVIDER_INACTIVE = 0;
/** @var null|communication_provider|user_provider|room_chat_provider|room_user_provider The provider class */
private communication_provider|user_provider|room_chat_provider|room_user_provider|null $provider = null;
/**
* Communication processor constructor.
*
* @param stdClass $instancedata The instance data object
*/
protected function __construct(
private stdClass $instancedata,
) {
$providercomponent = $this->instancedata->provider;
$providerclass = $this->get_classname_for_provider($providercomponent);
if (!class_exists($providerclass)) {
throw new \moodle_exception('communicationproviderclassnotfound', 'core_communication', '', $providerclass);
}
if (!is_a($providerclass, communication_provider::class, true)) {
// At the moment we only have one communication provider interface.
// In the future, we may have others, at which point we will support the newest first and
// emit a debugging notice for older ones.
throw new \moodle_exception('communicationproviderclassinvalid', 'core_communication', '', $providerclass);
}
$this->provider = $providerclass::load_for_instance($this);
}
/**
* Create communication instance.
*
* @param string $provider The communication provider
* @param int $instanceid The instance id
* @param string $component The component name
* @param string $instancetype The instance type
* @param string $roomname The room name
* @return processor|null
*/
public static function create_instance(
string $provider,
int $instanceid,
string $component,
string $instancetype,
string $roomname,
): ?self {
global $DB;
if ($provider === self::PROVIDER_NONE) {
return null;
}
$record = (object) [
'provider' => $provider,
'instanceid' => $instanceid,
'component' => $component,
'instancetype' => $instancetype,
'roomname' => $roomname,
'avatarfilename' => null,
'active' => self::PROVIDER_ACTIVE,
'avatarsynced' => 0,
];
$record->id = $DB->insert_record('communication', $record);
return new self($record);
}
/**
* Update the communication instance with any changes.
*
* @param null|string $provider The communication provider
* @param null|string $roomname The room name
*/
public function update_instance(
?string $provider = null,
?string $roomname = null,
): void {
global $DB;
if ($provider !== null) {
if ($provider === self::PROVIDER_NONE) {
$this->instancedata->active = self::PROVIDER_INACTIVE;
} else {
$this->instancedata->provider = $provider;
$this->instancedata->active = self::PROVIDER_ACTIVE;
}
}
if ($roomname !== null) {
$this->instancedata->roomname = $roomname;
}
$DB->update_record('communication', $this->instancedata);
}
/**
* Delete communication data.
*/
public function delete_instance(): void {
global $DB;
$DB->delete_records('communication', ['id' => $this->instancedata->id]);
}
/**
* Get non synced instance user ids for the instance.
*
* @param bool $synced The synced status
* @param bool $deleted The deleted status
* @return array
*/
public function get_instance_userids(bool $synced = false, bool $deleted = false): array {
global $DB;
return $DB->get_fieldset_select(
'communication_user',
'userid',
'commid = ? AND synced = ? AND deleted = ?',
[$this->instancedata->id, (int) $synced, (int) $deleted]
);
}
/**
* Get existing instance user ids.
*
* @return array
*/
public function get_all_userids_for_instance(): array {
global $DB;
return $DB->get_fieldset_select(
'communication_user',
'userid',
'commid = ?',
[$this->instancedata->id]
);
}
/**
* Create communication user record for mapping and sync.
*
* @param array $userids The user ids
*/
public function create_instance_user_mapping(array $userids): void {
global $DB;
// Check if user ids exits in existing user ids.
$useridstoadd = array_diff($userids, $this->get_all_userids_for_instance());
foreach ($useridstoadd as $userid) {
$record = (object) [
'commid' => $this->instancedata->id,
'userid' => $userid,
];
$DB->insert_record('communication_user', $record);
}
$this->mark_users_as_not_deleted($userids);
}
/**
* Mark users as not deleted for the instance.
*
* @param array $userids The user ids
*/
public function mark_users_as_not_deleted(array $userids): void {
global $DB;
if (empty($userids)) {
return;
}
$DB->set_field_select(
'communication_user',
'deleted',
0,
'commid = ? AND userid IN (' . implode(',', $userids) . ')',
[$this->instancedata->id]
);
}
/**
* Mark users as synced for the instance.
*
* @param array $userids The user ids
*/
public function mark_users_as_synced(array $userids): void {
global $DB;
if (empty($userids)) {
return;
}
$DB->set_field_select(
'communication_user',
'synced',
1,
'commid = ? AND userid IN (' . implode(',', $userids) . ')',
[$this->instancedata->id]
);
}
/**
* Reset users sync flag for the instance.
*
* @param array $userids The user ids
*/
public function reset_users_sync_flag(array $userids): void {
global $DB;
if (empty($userids)) {
return;
}
$DB->set_field_select(
'communication_user',
'synced',
0,
'commid = ? AND userid IN (' . implode(',', $userids) . ')',
[$this->instancedata->id]
);
}
/**
* Delete users flag for the instance users.
*
* @param array $userids The user ids
*/
public function add_delete_user_flag(array $userids): void {
global $DB;
if (empty($userids)) {
return;
}
$DB->set_field_select(
'communication_user',
'deleted',
1,
'commid = ? AND userid IN (' . implode(',', $userids) . ')',
[$this->instancedata->id]
);
}
/**
* Delete communication user record for userid.
*
* @param array $userids The user ids
*/
public function delete_instance_user_mapping(array $userids): void {
global $DB;
if (empty($userids)) {
return;
}
$DB->delete_records_select(
'communication_user',
'commid = ? AND userid IN (' . implode(',', $userids) . ')',
[$this->instancedata->id]
);
}
/**
* Delete communication user record for userid who are not synced.
*
* @param array $userids The user ids
*/
public function delete_instance_non_synced_user_mapping(array $userids): void {
global $DB;
if (empty($userids)) {
return;
}
$DB->delete_records_select(
'communication_user',
'commid = ? AND userid IN (' . implode(',', $userids) . ') AND synced = ?' ,
[$this->instancedata->id, 0]
);
}
/**
* Delete communication user record for instance.
*/
public function delete_user_mappings_for_instance(): void {
global $DB;
$DB->delete_records('communication_user', [
'commid' => $this->instancedata->id,
]);
}
/**
* Load communication instance by id.
*
* @param int $id The communication instance id
* @return processor|null
*/
public static function load_by_id(int $id): ?self {
global $DB;
$record = $DB->get_record('communication', ['id' => $id]);
if ($record && self::is_provider_enabled($record->provider)) {
return new self($record);
}
return null;
}
/**
* Load communication instance by instance id.
*
* @param string $component The component name
* @param string $instancetype The instance type
* @param int $instanceid The instance id
* @return processor|null
*/
public static function load_by_instance(
string $component,
string $instancetype,
int $instanceid
): ?self {
global $DB;
$record = $DB->get_record('communication', [
'instanceid' => $instanceid,
'component' => $component,
'instancetype' => $instancetype,
]);
if ($record && self::is_provider_enabled($record->provider)) {
return new self($record);
}
return null;
}
/**
* Check if communication instance is active.
*
* @return bool
*/
public function is_instance_active(): bool {
return $this->instancedata->active;
}
/**
* Get communication provider class name.
*
* @param string $component The component name.
* @return string
*/
private function get_classname_for_provider(string $component): string {
return "{$component}\\communication_feature";
}
/**
* Get communication instance id after creating the instance in communication table.
*
* @return int
*/
public function get_id(): int {
return $this->instancedata->id;
}
/**
* Get communication instance id.
*
* @return string
*/
public function get_component(): string {
return $this->instancedata->component;
}
/**
* Get communication provider.
*
* @return string|null
*/
public function get_provider(): ?string {
if ((int)$this->instancedata->active === self::PROVIDER_ACTIVE) {
return $this->instancedata->provider;
}
return self::PROVIDER_NONE;
}
/**
* Get room name.
*
* @return string|null
*/
public function get_room_name(): ?string {
return $this->instancedata->roomname;
}
/**
* Get communication instance id.
*
* @return room_chat_provider
*/
public function get_room_provider(): room_chat_provider {
$this->require_api_enabled();
$this->require_room_features();
return $this->provider;
}
/**
* Get communication instance id.
*
* @return user_provider
*/
public function get_user_provider(): user_provider {
$this->require_api_enabled();
$this->require_user_features();
return $this->provider;
}
/**
* Get communication instance id.
*
* @return room_user_provider
*/
public function get_room_user_provider(): room_user_provider {
$this->require_api_enabled();
$this->require_room_features();
$this->require_room_user_features();
return $this->provider;
}
/**
* Get communication provider for form feature.
*
* @param string $provider The provider name
* @param \MoodleQuickForm $mform The moodle form
*/
public static function set_proider_form_definition(string $provider, \MoodleQuickForm $mform): void {
$providerclass = "{$provider}\\communication_feature";
$providerclass::set_form_definition($mform);
}
/**
* Get communication instance for form feature.
*
* @return form_provider
*/
public function get_form_provider(): form_provider {
$this->requires_form_features();
return $this->provider;
}
/**
* Get communication instance id.
*
* @return bool
*/
public function supports_user_features(): bool {
return ($this->provider instanceof user_provider);
}
/**
* Get communication instance id.
*
* @return bool
*/
public function supports_room_user_features(): bool {
if (!$this->supports_user_features()) {
return false;
}
if (!$this->supports_room_features()) {
return false;
}
return ($this->provider instanceof room_user_provider);
}
/**
* Check form feature available.
*
* @return bool
*/
public function requires_form_features(): void {
if (!$this->supports_form_features()) {
throw new \coding_exception('Form features are not supported by the provider');
}
}
/**
* Check support for form feature.
*
* @return bool
*/
public function supports_form_features(): bool {
return ($this->provider instanceof form_provider);
}
/**
* Get communication instance id.
*/
public function require_user_features(): void {
if (!$this->supports_user_features()) {
throw new \coding_exception('User features are not supported by the provider');
}
}
/**
* Get communication instance id.
*
* @return bool
*/
public function supports_room_features(): bool {
return ($this->provider instanceof room_chat_provider);
}
/**
* Check if communication api is enabled.
*/
public function require_api_enabled(): void {
if (!api::is_available()) {
throw new \coding_exception('Communication API is not enabled, please enable it from experimental features');
}
}
/**
* Get communication instance id.
*/
public function require_room_features(): void {
if (!$this->supports_room_features()) {
throw new \coding_exception('room features are not supported by the provider');
}
}
/**
* Get communication instance id.
*/
public function require_room_user_features(): void {
if (!$this->supports_room_user_features()) {
throw new \coding_exception('room features are not supported by the provider');
}
}
/**
* Get communication instance id.
*
* @return bool|\stored_file
*/
public function get_avatar(): ?stored_file {
$fs = get_file_storage();
$file = $fs->get_file(
(\context_system::instance())->id,
'core_communication',
'avatar',
$this->instancedata->id,
'/',
$this->instancedata->avatarfilename,
);
return $file ?: null;
}
/**
* Set the avatar file name.
*
* @param string|null $filename
*/
public function set_avatar_filename(?string $filename): void {
global $DB;
$this->instancedata->avatarfilename = $filename;
$DB->set_field('communication', 'avatarfilename', $filename, ['id' => $this->instancedata->id]);
}
/**
* Get the avatar file name.
*
* @return string|null
*/
public function get_avatar_filename(): ?string {
return $this->instancedata->avatarfilename;
}
/**
* Check if the avatar has been synced with the provider.
*
* @return bool
*/
public function is_avatar_synced(): bool {
return (bool) $this->instancedata->avatarsynced;
}
/**
* Indicate if the avatar has been synced with the provider.
*
* @param boolean $synced True if avatar has been synced.
*/
public function set_avatar_synced_flag(bool $synced): void {
global $DB;
$this->instancedata->avatarsynced = (int) $synced;
$DB->set_field('communication', 'avatarsynced', (int) $synced, ['id' => $this->instancedata->id]);
}
/**
* Get a room url.
*
* @return string|null
*/
public function get_room_url(): ?string {
if ($this->provider && $this->is_instance_active()) {
return $this->get_room_provider()->get_chat_room_url();
}
return null;
}
/**
* Is communication provider enabled/disabled.
*
* @param string $provider provider component name
* @return bool
*/
public static function is_provider_enabled(string $provider): bool {
return \core\plugininfo\communication::is_plugin_enabled($provider);
}
}