first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
define("core_communication/providerchooser",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
/**
* Communication provider selection handler.
*
* @module core_communication/communicationchooser
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const Selectors_fields={selector:'[data-communicationchooser-field="selector"]',updateButton:'[data-communicationchooser-field="updateButton"]'};_exports.init=()=>{document.querySelector(Selectors_fields.selector).addEventListener("change",(e=>{const form=e.target.closest("form"),updateButton=form.querySelector(Selectors_fields.updateButton),url=new URL(form.action);form.action=url.toString(),updateButton.click()}))}}));
//# sourceMappingURL=providerchooser.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"providerchooser.min.js","sources":["../src/providerchooser.js"],"sourcesContent":["\n// This file is part of Moodle - http://moodle.org/ //\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Communication provider selection handler.\n *\n * @module core_communication/communicationchooser\n * @copyright 2023 Safat Shahin <safat.shahin@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst Selectors = {\n fields: {\n selector: '[data-communicationchooser-field=\"selector\"]',\n updateButton: '[data-communicationchooser-field=\"updateButton\"]',\n },\n};\n\n/**\n * Initialise the format chooser.\n */\nexport const init = () => {\n document.querySelector(Selectors.fields.selector).addEventListener('change', e => {\n const form = e.target.closest('form');\n const updateButton = form.querySelector(Selectors.fields.updateButton);\n const url = new URL(form.action);\n\n form.action = url.toString();\n updateButton.click();\n });\n};\n"],"names":["Selectors","selector","updateButton","document","querySelector","addEventListener","e","form","target","closest","url","URL","action","toString","click"],"mappings":";;;;;;;;MAuBMA,iBACM,CACJC,SAAU,+CACVC,aAAc,kEAOF,KAChBC,SAASC,cAAcJ,iBAAiBC,UAAUI,iBAAiB,UAAUC,UACnEC,KAAOD,EAAEE,OAAOC,QAAQ,QACxBP,aAAeK,KAAKH,cAAcJ,iBAAiBE,cACnDQ,IAAM,IAAIC,IAAIJ,KAAKK,QAEzBL,KAAKK,OAASF,IAAIG,WAClBX,aAAaY"}
+43
View File
@@ -0,0 +1,43 @@
// 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/>.
/**
* Communication provider selection handler.
*
* @module core_communication/communicationchooser
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const Selectors = {
fields: {
selector: '[data-communicationchooser-field="selector"]',
updateButton: '[data-communicationchooser-field="updateButton"]',
},
};
/**
* Initialise the format chooser.
*/
export const init = () => {
document.querySelector(Selectors.fields.selector).addEventListener('change', e => {
const form = e.target.closest('form');
const updateButton = form.querySelector(Selectors.fields.updateButton);
const url = new URL(form.action);
form.action = url.toString();
updateButton.click();
});
};
@@ -0,0 +1,139 @@
<?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\admin;
use admin_setting;
use core_plugin_manager;
use core_text;
use html_table;
use html_table_row;
use html_writer;
use moodle_url;
/**
* Communication providers manager. Allow enable/disable communication providers and jump to settings.
*
* @package core_communication
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @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'),
'',
''
);
}
public function get_setting(): bool {
return true;
}
public function write_setting($data): string {
// Do not write any setting.
return '';
}
public function output_html($data, $query = ''): string {
global $OUTPUT;
$pluginmanager = core_plugin_manager::instance();
$plugins = $pluginmanager->get_plugins_of_type('communication');
if (empty($plugins)) {
return get_string('nocommunicationprovider', 'core_communication');
}
$table = new html_table();
$table->head = [
get_string('name'),
get_string('enable'),
get_string('settings'),
get_string('uninstallplugin', 'core_admin'),
];
$table->align = ['left', 'center', 'center', 'center'];
$table->attributes['class'] = 'managecommunicationtable generaltable admintable';
$table->data = [];
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
) {
$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'])
);
} else {
$class = 'dimmed_text';
$hideshow = html_writer::link(
$actionurl->out(false, ['action' => 'enable']),
$OUTPUT->pix_icon('t/show', get_string('enable'), 'moodle', ['class' => 'iconsmall'])
);
}
$settings = '';
if ($plugin->get_settings_url()) {
$settings = html_writer::link($plugin->get_settings_url(), get_string('settings'));
}
$uninstall = '';
if (
$uninstallurl = core_plugin_manager::instance()->get_uninstall_url(
'communication_' . $plugin->name,
'manage'
)
) {
$uninstall = html_writer::link($uninstallurl, get_string('uninstallplugin', 'core_admin'));
}
$row = new html_table_row([$strtypename, $hideshow, $settings, $uninstall]);
if ($class) {
$row->attributes['class'] = $class;
}
$table->data[] = $row;
}
return highlight($query, html_writer::table($table));
}
public function is_related($query): bool {
if (parent::is_related($query)) {
return true;
}
$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
) {
return true;
}
}
return false;
}
}
+833
View File
@@ -0,0 +1,833 @@
<?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 core\context;
use core_communication\task\add_members_to_room_task;
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\synchronise_provider_task;
use core_communication\task\update_room_task;
use core_communication\task\update_room_membership_task;
use stdClass;
/**
* Class api is the public endpoint of the communication api. This class is the point of contact for api usage.
*
* Communication api allows to add ad-hoc tasks to the queue to perform actions on the communication providers. This api will
* not allow any immediate actions to be performed on the communication providers. It will only add the tasks to the queue. The
* exception has been made for deletion of members in case of deleting the user. This is because the user will not be available.
* The member management api part allows run actions immediately if required.
*
* Communication api does allow to have form elements related to communication api in the required forms. This is done by using
* the form_definition method. This method will add the form elements to the form.
*
* @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 api {
/**
* @var null|processor $communication The communication settings object
*/
private ?processor $communication;
/**
* Communication handler constructor to manage and handle all communication related actions.
*
* This class is the entrypoint for all kinda usages.
* It will be used by the other api to manage the communication providers.
*
* @param context $context The context of the item for the instance
* @param string $component The component of the item for the instance
* @param string $instancetype The type of the item for the instance
* @param int $instanceid The id of the instance
* @param string|null $provider The provider type - if null will load for this context's active provider.
*
*/
private function __construct(
private context $context,
private string $component,
private string $instancetype,
private int $instanceid,
private ?string $provider = null,
) {
$this->communication = processor::load_by_instance(
context: $context,
component: $component,
instancetype: $instancetype,
instanceid: $instanceid,
provider: $provider,
);
}
/**
* Get the communication processor object.
*
* @param context $context The context of the item for the instance
* @param string $component The component of the item for the instance
* @param string $instancetype The type of the item for the instance
* @param int $instanceid The id of the instance
* @param string|null $provider The provider type - if null will load for this context's active provider.
* @return api
*/
public static function load_by_instance(
context $context,
string $component,
string $instancetype,
int $instanceid,
?string $provider = null,
): self {
return new self(
context: $context,
component: $component,
instancetype: $instancetype,
instanceid: $instanceid,
provider: $provider,
);
}
/**
* Reload in the internal instance data.
*/
public function reload(): void {
$this->communication = processor::load_by_instance(
context: $this->context,
component: $this->component,
instancetype: $this->instancetype,
instanceid: $this->instanceid,
provider: $this->provider,
);
}
/**
* Return the underlying communication processor object.
*
* @return ?processor
*/
public function get_processor(): ?processor {
return $this->communication;
}
/**
* Return the room provider.
*
* @return \core_communication\room_chat_provider
*/
public function get_room_provider(): \core_communication\room_chat_provider {
return $this->communication->get_room_provider();
}
/**
* Return the user provider.
*
* @return \core_communication\user_provider
*/
public function get_user_provider(): \core_communication\user_provider {
return $this->communication->get_user_provider();
}
/**
* Return the room user provider.
*
* @return \core_communication\room_user_provider
*/
public function get_room_user_provider(): \core_communication\room_user_provider {
return $this->communication->get_room_user_provider();
}
/**
* Return the form provider.
*
* @return \core_communication\form_provider
*/
public function get_form_provider(): \core_communication\form_provider {
return $this->communication->get_form_provider();
}
/**
* Check if the communication api is enabled.
*/
public static function is_available(): bool {
return (bool) get_config('core', 'enablecommunicationsubsystem');
}
/**
* Get the communication room url.
*
* @return string|null
*/
public function get_communication_room_url(): ?string {
return $this->communication?->get_room_url();
}
/**
* Get the list of plugins for form selection.
*
* @return array
*/
public static function get_communication_plugin_list_for_form(): array {
// Add the option to have communication disabled.
$selection[processor::PROVIDER_NONE] = get_string('nocommunicationselected', 'communication');
$communicationplugins = \core\plugininfo\communication::get_enabled_plugins();
foreach ($communicationplugins as $pluginname => $notusing) {
$provider = 'communication_' . $pluginname;
if (processor::is_provider_available($provider)) {
$selection[$provider] = get_string('pluginname', 'communication_' . $pluginname);
}
}
return $selection;
}
/**
* Get the enabled communication providers and default provider according to the selected provider.
*
* @param string|null $selecteddefaulprovider
* @return array
*/
public static function get_enabled_providers_and_default(string $selecteddefaulprovider = null): array {
$communicationproviders = self::get_communication_plugin_list_for_form();
$defaulprovider = processor::PROVIDER_NONE;
if (!empty($selecteddefaulprovider) && array_key_exists($selecteddefaulprovider, $communicationproviders)) {
$defaulprovider = $selecteddefaulprovider;
}
return [$communicationproviders, $defaulprovider];
}
/**
* Define the form elements for the communication api.
* This method will be called from the form definition method of the instance.
*
* @param \MoodleQuickForm $mform The form element
* @param string $selectdefaultcommunication The default selected communication provider in the form field
*/
public function form_definition(
\MoodleQuickForm $mform,
string $selectdefaultcommunication = processor::PROVIDER_NONE
): void {
global $PAGE;
[$communicationproviders, $defaulprovider] = self::get_enabled_providers_and_default($selectdefaultcommunication);
$PAGE->requires->js_call_amd('core_communication/providerchooser', 'init');
// List the communication providers.
$mform->addElement(
'select',
'selectedcommunication',
get_string('selectcommunicationprovider', 'communication'),
$communicationproviders,
['data-communicationchooser-field' => 'selector'],
);
$mform->addHelpButton('selectedcommunication', 'selectcommunicationprovider', 'communication');
$mform->setDefault('selectedcommunication', $defaulprovider);
$mform->registerNoSubmitButton('updatecommunicationprovider');
$mform->addElement(
'submit',
'updatecommunicationprovider',
'update communication',
['data-communicationchooser-field' => 'updateButton', 'class' => 'd-none']
);
// Just a placeholder for the communication options.
$mform->addElement('hidden', 'addcommunicationoptionshere');
$mform->setType('addcommunicationoptionshere', PARAM_BOOL);
}
/**
* Set the form definitions for the plugins.
*
* @param \MoodleQuickForm $mform The moodle form
* @param string $provider The provider name
*/
public function form_definition_for_provider(\MoodleQuickForm $mform, string $provider = processor::PROVIDER_NONE): void {
if ($provider === processor::PROVIDER_NONE) {
return;
}
// Room name for the communication provider.
$mform->insertElementBefore(
$mform->createElement(
'text',
$provider . 'roomname',
get_string('communicationroomname', 'communication'),
'maxlength="100" size="20"'
),
'addcommunicationoptionshere'
);
$mform->setType($provider . 'roomname', PARAM_TEXT);
$mform->insertElementBefore(
$mform->createElement(
'static',
'communicationroomnameinfo',
'',
get_string('communicationroomnameinfo', 'communication'),
),
'addcommunicationoptionshere',
);
processor::set_provider_specific_form_definition($provider, $mform);
}
/**
* Get the avatar file.
*
* @return null|\stored_file
*/
public function get_avatar(): ?\stored_file {
$filename = $this->communication->get_avatar_filename();
if ($filename === null) {
return null;
}
$fs = get_file_storage();
$args = (array) $this->get_avatar_filerecord($filename);
return $fs->get_file(...$args) ?: null;
}
/**
* Get the avatar file record for the avatar for filesystem.
*
* @param string $filename The filename of the avatar
* @return stdClass
*/
protected function get_avatar_filerecord(string $filename): stdClass {
return (object) [
'contextid' => \core\context\system::instance()->id,
'component' => 'core_communication',
'filearea' => 'avatar',
'itemid' => $this->communication->get_id(),
'filepath' => '/',
'filename' => $filename,
];
}
/**
* Get the avatar file.
*
* If null is set, then delete the old area file and set the avatarfilename to null.
* This will make sure the plugin api deletes the avatar from the room.
*
* @param null|\stored_file $avatar The stored file for the avatar
* @return bool
*/
public function set_avatar(?\stored_file $avatar): bool {
$currentfilename = $this->communication->get_avatar_filename();
if ($avatar === null && empty($currentfilename)) {
return false;
}
$currentfilerecord = $this->get_avatar();
if ($avatar && $currentfilerecord) {
$currentfilehash = $currentfilerecord->get_contenthash();
$updatedfilehash = $avatar->get_contenthash();
// No update required.
if ($currentfilehash === $updatedfilehash) {
return false;
}
}
$context = \core\context\system::instance();
$fs = get_file_storage();
$fs->delete_area_files(
$context->id,
'core_communication',
'avatar',
$this->communication->get_id()
);
if ($avatar) {
$fs->create_file_from_storedfile(
$this->get_avatar_filerecord($avatar->get_filename()),
$avatar,
);
$this->communication->set_avatar_filename($avatar->get_filename());
} else {
$this->communication->set_avatar_filename(null);
}
// Indicate that we need to sync the avatar when the update task is run.
$this->communication->set_avatar_synced_flag(false);
return true;
}
/**
* A helper to fetch the room name
*
* @return string
*/
public function get_room_name(): string {
if (!$this->communication) {
return '';
}
return $this->communication->get_room_name();
}
/**
* Set the form data if the data is already available.
*
* @param \stdClass $instance The instance object
*/
public function set_data(\stdClass $instance): void {
if (!empty($instance->id) && $this->communication) {
$instance->selectedcommunication = $this->communication->get_provider();
$roomnameidentifier = $this->get_provider() . 'roomname';
$instance->$roomnameidentifier = $this->communication->get_room_name();
$this->communication->get_form_provider()->set_form_data($instance);
}
}
/**
* Get the communication provider.
*
* @return string
*/
public function get_provider(): string {
if (!$this->communication) {
return '';
}
return $this->communication->get_provider();
}
/**
* Configure the room and membership by provider selected for the communication instance.
*
* This method will add a task to the queue to configure the room and membership by comparing the change of provider.
* There are some major cases to consider for this method to allow minimum duplication when this api is used.
* Some of the major cases are:
* 1. If the communication instance is not created at all, then create it and add members.
* 2. If the current provider is none and the new provider is also none, then nothing to do.
* 3. If the current and existing provider is the same, don't need to do anything.
* 4. If provider set to none, remove all the members.
* 5. If previous provider was not none and current provider is not none, but a different provider, remove members and add
* for the new one.
* 6. If previous provider was none and current provider is not none, don't need to remove, just
* update the selected provider and add users to that provider. Do not queue the task to add members to room as the room
* might not have created yet. The add room task adds the task to add members to room anyway.
* 7. If it's a new provider, never used/created, now create the room after considering all these cases for a new provider.
*
* @param string $provider The provider name
* @param \stdClass $instance The instance object
* @param string $communicationroomname The communication room name
* @param array $users The user ids to add to the room
* @param null|\stored_file $instanceimage The stored file for the avatar
* @param bool $queue Queue the task for the provider room or not
*/
public function configure_room_and_membership_by_provider(
string $provider,
stdClass $instance,
string $communicationroomname,
array $users,
?\stored_file $instanceimage = null,
bool $queue = true,
): void {
// If the current provider is inactive and the new provider is also none, then nothing to do.
if (
$this->communication !== null &&
$this->communication->get_provider_status() === processor::PROVIDER_INACTIVE &&
$provider === processor::PROVIDER_NONE
) {
return;
}
// If provider set to none, remove all the members.
if (
$this->communication !== null &&
$this->communication->get_provider_status() === processor::PROVIDER_ACTIVE &&
$provider === processor::PROVIDER_NONE
) {
$this->remove_all_members_from_room();
$this->update_room(
active: processor::PROVIDER_INACTIVE,
communicationroomname: $communicationroomname,
avatar: $instanceimage,
instance: $instance,
queue: $queue,
);
return;
}
if (
// If previous provider was active and not none and current provider is not none, but a different provider,
// remove members and de-activate the previous provider.
$this->communication !== null &&
$this->communication->get_provider_status() === processor::PROVIDER_ACTIVE &&
$provider !== $this->get_provider()
) {
$this->remove_all_members_from_room();
// Now deactivate the previous provider.
$this->update_room(
active: processor::PROVIDER_INACTIVE,
instance: $instance,
queue: $queue,
);
}
// Now re-init the constructor for the new provider.
$this->__construct(
context: $this->context,
component: $this->component,
instancetype: $this->instancetype,
instanceid: $this->instanceid,
provider: $provider,
);
// If it's a new provider, never used/created, now create the room.
if ($this->communication === null) {
$this->create_and_configure_room(
communicationroomname: $communicationroomname,
avatar: $instanceimage,
instance: $instance,
queue: $queue,
);
$queueusertask = false;
} else {
// Otherwise update the room.
$this->update_room(
active: processor::PROVIDER_ACTIVE,
communicationroomname: $communicationroomname,
avatar: $instanceimage,
instance: $instance,
queue: $queue,
);
$queueusertask = true;
}
// Now add the members.
$this->add_members_to_room(
userids: $users,
queue: $queueusertask,
);
}
/**
* Create a communication ad-hoc task for create operation.
* This method will add a task to the queue to create the room.
*
* @param string $communicationroomname The communication room name
* @param null|\stored_file $avatar The stored file for the avatar
* @param \stdClass|null $instance The actual instance object
* @param bool $queue Whether to queue the task or not
*/
public function create_and_configure_room(
string $communicationroomname,
?\stored_file $avatar = null,
?\stdClass $instance = null,
bool $queue = true,
): void {
if ($this->provider === processor::PROVIDER_NONE || $this->provider === '') {
return;
}
// Create communication record.
$this->communication = processor::create_instance(
context: $this->context,
provider: $this->provider,
instanceid: $this->instanceid,
component: $this->component,
instancetype: $this->instancetype,
roomname: $communicationroomname,
);
// Update provider record from form data.
if ($instance !== null) {
$this->communication->get_form_provider()->save_form_data($instance);
}
// Set the avatar.
if (!empty($avatar)) {
$this->set_avatar($avatar);
}
// Nothing else to do if the queue is false.
if (!$queue) {
return;
}
// Add ad-hoc task to create the provider room.
create_and_configure_room_task::queue(
$this->communication,
);
}
/**
* Create a communication ad-hoc task for update operation.
* This method will add a task to the queue to update the room.
*
* @param null|int $active The selected active state of the provider
* @param null|string $communicationroomname The communication room name
* @param null|\stored_file $avatar The stored file for the avatar
* @param \stdClass|null $instance The actual instance object
* @param bool $queue Whether to queue the task or not
*/
public function update_room(
?int $active = null,
?string $communicationroomname = null,
?\stored_file $avatar = null,
?\stdClass $instance = null,
bool $queue = true,
): void {
if (!$this->communication) {
return;
}
// If the provider is none, we don't need to do anything from room point of view.
if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
return;
}
$roomnamechange = null;
$activestatuschange = null;
// Check if the room name is being changed.
if (
$communicationroomname !== null &&
$communicationroomname !== $this->communication->get_room_name()
) {
$roomnamechange = $communicationroomname;
}
// Check if the active status of the provider is being changed.
if (
$active !== null &&
$active !== $this->communication->is_instance_active()
) {
$activestatuschange = $active;
}
if ($roomnamechange !== null || $activestatuschange !== null) {
$this->communication->update_instance(
active: $active,
roomname: $communicationroomname,
);
}
// Update provider record from form data.
if ($instance !== null) {
$this->communication->get_form_provider()->save_form_data($instance);
}
// Update the avatar.
// If the value is `null`, then unset the avatar.
$this->set_avatar($avatar);
// Nothing else to do if the queue is false.
if (!$queue) {
return;
}
// Always queue a room update, even if none of the above standard fields have changed.
// It is possible for providers to have custom fields that have been updated.
update_room_task::queue(
$this->communication,
);
}
/**
* Create a communication ad-hoc task for delete operation.
* This method will add a task to the queue to delete the room.
*/
public function delete_room(): void {
if ($this->communication !== null) {
// Add the ad-hoc task to remove the room data from the communication table and associated provider actions.
delete_room_task::queue(
$this->communication,
);
}
}
/**
* Create a communication ad-hoc task for add members operation and add the user mapping.
*
* This method will add a task to the queue to add 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 add_members_to_room(array $userids, bool $queue = true): void {
// No communication object? something not done right.
if (!$this->communication) {
return;
}
// No user IDs or this provider does not manage users? No action required.
if (empty($userids) || !$this->communication->supports_user_features()) {
return;
}
$this->communication->create_instance_user_mapping($userids);
if ($queue) {
add_members_to_room_task::queue(
$this->communication
);
}
}
/**
* 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.
*
* This method will add a task to the queue to remove the room users.
*
* @param array $userids The user ids to remove from the room
* @param bool $queue Whether to queue the task or not
*/
public function remove_members_from_room(array $userids, bool $queue = true): void {
// No communication object? something not done right.
if (!$this->communication) {
return;
}
$provider = $this->communication->get_provider();
if ($provider === processor::PROVIDER_NONE) {
return;
}
// No user IDs or this provider does not manage users? No action required.
if (empty($userids) || !$this->communication->supports_user_features()) {
return;
}
$this->communication->add_delete_user_flag($userids);
if ($queue) {
remove_members_from_room::queue(
$this->communication
);
}
}
/**
* Remove all users from the room.
*
* @param bool $queue Whether to queue the task or not
*/
public function remove_all_members_from_room(bool $queue = true): void {
// No communication object? something not done right.
if (!$this->communication) {
return;
}
if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
return;
}
// This provider does not manage users? No action required.
if (!$this->communication->supports_user_features()) {
return;
}
$this->communication->add_delete_user_flag($this->communication->get_all_userids_for_instance());
if ($queue) {
remove_members_from_room::queue(
$this->communication
);
}
}
/**
* Display the communication room status notification.
*/
public function show_communication_room_status_notification(): void {
// No communication, no room.
if (!$this->communication) {
return;
}
if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
return;
}
$roomstatus = $this->get_communication_room_url()
? constants::COMMUNICATION_STATUS_READY
: constants::COMMUNICATION_STATUS_PENDING;
$pluginname = get_string('pluginname', $this->get_provider());
$message = get_string('communicationroom' . $roomstatus, 'communication', $pluginname);
// We only show the ready notification once per user.
// We check this with a custom user preference.
$roomreadypreference = "{$this->component}_{$this->instancetype}_{$this->instanceid}_room_ready";
switch ($roomstatus) {
case constants::COMMUNICATION_STATUS_PENDING:
\core\notification::add($message, \core\notification::INFO);
unset_user_preference($roomreadypreference);
break;
case constants::COMMUNICATION_STATUS_READY:
if (empty(get_user_preferences($roomreadypreference))) {
\core\notification::add($message, \core\notification::SUCCESS);
set_user_preference($roomreadypreference, true);
}
break;
}
}
/**
* Add the task to sync the provider data with local Moodle data.
*/
public function sync_provider(): void {
// No communication, return.
if (!$this->communication) {
return;
}
if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
return;
}
synchronise_provider_task::queue(
$this->communication
);
}
}
@@ -0,0 +1,45 @@
<?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;
/**
* A base communication provider.
*
* This interface should be used to declare support for the instantiation method for communication providers.
*
* Every communication provider must, as a minimum, implement this provider.
*
* @package core_communication
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface communication_provider {
/**
* A base communication provider.
*
* @param processor $communication The communication object
*/
public static function load_for_instance(processor $communication): self;
/**
* Check if the provider is configured or not.
*
* This method is intended to check if the plugin have got any settings and if all the settings are set properly.
* This checking helps to reduce errors in future when a communication instance is added for the provider and not configured.
*/
public static function is_configured(): bool;
}
+45
View File
@@ -0,0 +1,45 @@
<?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;
/**
* Constants for communication api.
*
* @package core_communication
* @copyright 2024 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class constants {
/** @var string GROUP_COMMUNICATION_INSTANCETYPE The group communication instance type. */
public const GROUP_COMMUNICATION_INSTANCETYPE = 'groupcommunication';
/** @var string GROUP_COMMUNICATION_COMPONENT The group communication component. */
public const GROUP_COMMUNICATION_COMPONENT = 'core_group';
/** @var string COURSE_COMMUNICATION_INSTANCETYPE The course communication instance type. */
public const COURSE_COMMUNICATION_INSTANCETYPE = 'coursecommunication';
/** @var string COURSE_COMMUNICATION_COMPONENT The course communication component. */
public const COURSE_COMMUNICATION_COMPONENT = 'core_course';
/** @var string COMMUNICATION_STATUS_PENDING The communication status pending. */
public const COMMUNICATION_STATUS_PENDING = 'pending';
/** @var string COMMUNICATION_STATUS_READY The communication status sent. */
public const COMMUNICATION_STATUS_READY = 'ready';
}
@@ -0,0 +1,140 @@
<?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/>.
/**
* Configure communication for a given instance - the form definition.
*
* @package core_communication
* @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_communication\form;
use core\context;
use stdClass;
defined('MOODLE_INTERNAL') || die();
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.
*/
protected $communication;
/**
* Class constructor
*
* @param context $context Context object
* @param int|null $instanceid Instance ID
* @param string|null $instancetype Instance type
* @param string|null $component Component name
* @param string|null $selectedcommunication Selected communication service (provider)
* @param stdClass|null $instancedata Instance data
*/
public function __construct(
context $context,
?int $instanceid = null,
?string $instancetype = null,
?string $component = null,
?string $selectedcommunication = null,
?stdClass $instancedata = null,
) {
parent::__construct(
null,
[
'context' => $context,
'instanceid' => $instanceid,
'instancetype' => $instancetype,
'component' => $component,
'selectedcommunication' => $selectedcommunication,
'instancedata' => $instancedata,
],
);
}
/**
* Defines the form fields.
*/
public function definition() {
$mform = $this->_form;
$context = $this->_customdata['context'];
$instanceid = $this->_customdata['instanceid'];
$instancetype = $this->_customdata['instancetype'];
$component = $this->_customdata['component'];
$instancedata = $this->_customdata['instancedata'];
// Add communication plugins to the form.
$this->communication = \core_communication\api::load_by_instance(
context: $context,
component: $component,
instancetype: $instancetype,
instanceid: $instanceid,
provider: $this->_customdata['selectedcommunication'],
);
$this->communication->form_definition($mform);
$this->communication->set_data($instancedata);
$this->set_form_definition_for_provider();
// Form buttons.
$buttonarray = [];
$buttonarray[] = $mform->createElement('submit', 'saveandreturn', get_string('savechanges'));
$buttonarray[] = $mform->createElement('cancel');
$mform->addGroup($buttonarray, 'buttonar', '', [' '], false);
$mform->closeHeaderBefore('buttonar');
// Hidden elements.
$mform->addElement('hidden', 'contextid', $context->id);
$mform->setType('contextid', PARAM_INT);
$mform->addElement('hidden', 'instanceid', $instanceid);
$mform->setType('instanceid', PARAM_INT);
$mform->addElement('hidden', 'instancetype', $instancetype);
$mform->setType('instancetype', PARAM_TEXT);
$mform->addElement('hidden', 'component', $component);
$mform->setType('component', PARAM_TEXT);
// Finally set the current form data.
$this->set_data($instancedata);
}
/**
* Defines the requested/current provider
*
* Get the selected communication service (provider),
* and then use it to show the provider form fields.
*/
private function set_form_definition_for_provider(): void {
$instancedata = $this->_customdata['instancedata'];
if ($selectedcommunication = $this->_customdata['selectedcommunication']) {
// First is to check whether the selected communication was selected from the form.
$provider = $selectedcommunication;
} else if (isset($instancedata->selectedcommunication)) {
// If the form is not yet submitted, get the value from the DB.
$provider = $instancedata->selectedcommunication;
} else {
// Otherwise, set to PROVIDER_NONE.
$provider = \core_communication\processor::PROVIDER_NONE;
}
$this->communication->form_definition_for_provider($this->_form, $provider);
}
}
+49
View File
@@ -0,0 +1,49 @@
<?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;
/**
* Interface form_provider to manage communication provider form options from provider plugins.
*
* Every provider plugin should implement this class to return the implemented form elements for custom data.
*
* @package core_communication
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @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.
*
* @param \stdClass $instance The actual instance to set the data
*/
public function save_form_data(\stdClass $instance): void;
/**
* Set the form data to the instance if any data is available.
*
* @param \stdClass $instance The actual instance to set the data
*/
public function set_form_data(\stdClass $instance): void;
/**
* Set the form definitions.
*
* @param \MoodleQuickForm $mform The form object
*/
public static function set_form_definition(\MoodleQuickForm $mform): void;
}
+575
View File
@@ -0,0 +1,575 @@
<?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 context;
use stdClass;
/**
* Helper method for communication.
*
* @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 helper {
/**
* Load the communication instance for group id.
*
* @param int $groupid The group id
* @param context $context The context, to make sure any instance using group can load the communication instance
* @return api The communication instance.
*/
public static function load_by_group(int $groupid, context $context): api {
return \core_communication\api::load_by_instance(
context: $context,
component: constants::GROUP_COMMUNICATION_COMPONENT,
instancetype: constants::GROUP_COMMUNICATION_INSTANCETYPE,
instanceid: $groupid,
);
}
/**
* Load the communication instance for course id.
*
* @param int $courseid The course id
* @param \context $context The context
* @param string|null $provider The provider name
* @return api The communication instance
*/
public static function load_by_course(
int $courseid,
\context $context,
?string $provider = null,
): api {
return \core_communication\api::load_by_instance(
context: $context,
component: constants::COURSE_COMMUNICATION_COMPONENT,
instancetype: constants::COURSE_COMMUNICATION_INSTANCETYPE,
instanceid: $courseid,
provider: $provider,
);
}
/**
* Communication api call to create room for a group if course has group mode enabled.
*
* @param int $courseid The course id.
* @return stdClass
*/
public static function get_course(int $courseid): stdClass {
global $DB;
return $DB->get_record(
table: 'course',
conditions: ['id' => $courseid],
strictness: MUST_EXIST,
);
}
/**
* Is group mode enabled for the course.
*
* @param stdClass $course The course object
*/
public static function is_group_mode_enabled_for_course(stdClass $course): bool {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return false;
}
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
return (int)$groupmode !== NOGROUPS;
}
/**
* Helper to update room membership according to action passed.
* This method will help reduce a large amount of duplications of code in different places in core.
*
* @param \stdClass $course The course object.
* @param array $userids The user ids to add to the communication room.
* @param string $memberaction The action to perform on the communication room.
*/
public static function update_course_communication_room_membership(
\stdClass $course,
array $userids,
string $memberaction,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
// Validate communication api action.
$roomuserprovider = new \ReflectionClass(room_user_provider::class);
if (!$roomuserprovider->hasMethod($memberaction)) {
throw new \coding_exception('Invalid action provided.');
}
$coursecontext = \context_course::instance(courseid: $course->id);
$communication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
// Check we have communication correctly set up before proceeding.
if ($communication->get_processor() === null) {
return;
}
// Get the group mode for this course.
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
if ((int)$groupmode === NOGROUPS) {
// If group mode is not set, then just handle the update normally for these users.
$communication->$memberaction($userids);
} else {
// If group mode is set, then handle the update for these users with repect to the group they are in.
$coursegroups = groups_get_all_groups(courseid: $course->id);
$usershandled = [];
// Filter all the users that have the capability to access all groups.
$allaccessgroupusers = self::get_users_has_access_to_all_groups(
userids: $userids,
courseid: $course->id,
);
foreach ($coursegroups as $coursegroup) {
// Get all group members.
$groupmembers = groups_get_members(groupid: $coursegroup->id, fields: 'u.id');
$groupmembers = array_column($groupmembers, 'id');
// Find the common user ids between the group members and incoming userids.
$groupuserstohandle = array_intersect(
$groupmembers,
$userids,
);
// Add users who have the capability to access this group (and haven't been added already).
foreach ($allaccessgroupusers as $allaccessgroupuser) {
if (!in_array($allaccessgroupuser, $groupuserstohandle, true)) {
$groupuserstohandle[] = $allaccessgroupuser;
}
}
// Keep track of the users we have handled already.
$usershandled = array_merge($usershandled, $groupuserstohandle);
// Let's check if we need to add/remove members from room because of a role change.
// First, get all the instance users for this group.
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$instanceusers = $communication->get_processor()->get_all_userids_for_instance();
// The difference between the instance users and the group members are the ones we want to check.
$roomuserstocheck = array_diff(
$instanceusers,
$groupmembers
);
if (!empty($roomuserstocheck)) {
// Check if they still have the capability to keep their access in the room.
$userslostcaps = array_diff(
$roomuserstocheck,
self::get_users_has_access_to_all_groups(
userids: $roomuserstocheck,
courseid: $course->id,
),
);
// Remove users who no longer have the capability.
if (!empty($userslostcaps)) {
$communication->remove_members_from_room(userids: $userslostcaps);
}
}
// Check if we have to add any room members who have gained the capability.
$usersgainedcaps = array_diff(
$allaccessgroupusers,
$instanceusers,
);
// If we have users, add them to the room.
if (!empty($usersgainedcaps)) {
$communication->add_members_to_room(userids: $usersgainedcaps);
}
// Finally, trigger the update task for the users who need to be handled.
$communication->$memberaction($groupuserstohandle);
}
// If the user was not in any group, but an update/remove action was requested for the user,
// then the user must have had a role with the capablity, but made a regular user.
$usersnothandled = array_diff($userids, $usershandled);
// These users are not handled and not in any group, so logically these users lost their permission to stay in the room.
foreach ($coursegroups as $coursegroup) {
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$communication->remove_members_from_room(userids: $usersnothandled);
}
}
}
/**
* Get users with the capability to access all groups.
*
* @param array $userids user ids to check the permission
* @param int $courseid course id
* @return array of userids
*/
public static function get_users_has_access_to_all_groups(
array $userids,
int $courseid
): array {
$allgroupsusers = [];
$context = \context_course::instance(courseid: $courseid);
foreach ($userids as $userid) {
if (
has_capability(
capability: 'moodle/site:accessallgroups',
context: $context,
user: $userid,
)
) {
$allgroupsusers[] = $userid;
}
}
return $allgroupsusers;
}
/**
* Get the course communication url according to course setup.
*
* @param stdClass $course The course object.
* @return string The communication room url.
*/
public static function get_course_communication_url(stdClass $course): string {
// If it's called from site context, then just return.
if ($course->id === SITEID) {
return '';
}
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return '';
}
$url = '';
// Get the group mode for this course.
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
$coursecontext = \context_course::instance(courseid: $course->id);
// If group mode is not set then just handle the course communication for these users.
if ((int)$groupmode === NOGROUPS) {
$communication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$url = $communication->get_communication_room_url();
} else {
// If group mode is set then handle the group communication rooms for these users.
$coursegroups = groups_get_all_groups(courseid: $course->id);
$numberofgroups = count($coursegroups);
// If no groups available, nothing to show.
if ($numberofgroups === 0) {
return '';
}
$readygroups = [];
foreach ($coursegroups as $coursegroup) {
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$roomstatus = $communication->get_communication_room_url() ? 'ready' : 'pending';
if ($roomstatus === 'ready') {
$readygroups[$communication->get_processor()->get_id()] = $communication->get_communication_room_url();
}
}
if (!empty($readygroups)) {
$highestkey = max(array_keys($readygroups));
$url = $readygroups[$highestkey];
}
}
return empty($url) ? '' : $url;
}
/**
* Get the enrolled users for course.
*
* @param stdClass $course The course object.
* @param bool $onlyactive Only enrolments that are active (e.g. not suspended).
* @return array
*/
public static function get_enrolled_users_for_course(stdClass $course, bool $onlyactive = true): array {
global $CFG;
require_once($CFG->libdir . '/enrollib.php');
return array_column(
enrol_get_course_users(courseid: $course->id, onlyactive: $onlyactive),
'id',
);
}
/**
* Get the course communication status notification for course.
*
* @param \stdClass $course The course object.
*/
public static function get_course_communication_status_notification(\stdClass $course): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
// Get the group mode for this course.
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
$coursecontext = \context_course::instance(courseid: $course->id);
// If group mode is not set then just handle the course communication for these users.
if ((int)$groupmode === NOGROUPS) {
$communication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$communication->show_communication_room_status_notification();
} else {
// If group mode is set then handle the group communication rooms for these users.
$coursegroups = groups_get_all_groups(courseid: $course->id);
$numberofgroups = count($coursegroups);
// If no groups available, nothing to show.
if ($numberofgroups === 0) {
return;
}
$numberofreadygroups = 0;
foreach ($coursegroups as $coursegroup) {
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$roomstatus = $communication->get_communication_room_url() ? 'ready' : 'pending';
switch ($roomstatus) {
case 'ready':
$numberofreadygroups ++;
break;
case 'pending':
$pendincommunicationobject = $communication;
break;
}
}
if ($numberofgroups === $numberofreadygroups) {
$communication->show_communication_room_status_notification();
} else {
$pendincommunicationobject->show_communication_room_status_notification();
}
}
}
/**
* Update course communication according to course data.
* Course can have course or group rooms. Group mode enabling will create rooms for groups.
*
* @param stdClass $course The course data
* @param bool $changesincoursecat Whether the course moved to a different category
*/
public static function update_course_communication_instance(
stdClass $course,
bool $changesincoursecat
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
// Check if provider is selected.
$provider = $course->selectedcommunication ?? null;
// If the course moved to hidden category, set provider to none.
if ($changesincoursecat && empty($course->visible)) {
$provider = processor::PROVIDER_NONE;
}
// Get the course context.
$coursecontext = \context_course::instance(courseid: $course->id);
// Get the course image.
$courseimage = course_get_courseimage(course: $course);
// Get the course communication instance.
$coursecommunication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
// Attempt to get the communication provider if it wasn't provided in the data.
if (empty($provider)) {
$provider = $coursecommunication->get_provider();
}
$roomnameidenfier = $provider . 'roomname';
// Determine the communication room name if none was provided and add it to the course data.
if (empty($course->$roomnameidenfier)) {
$course->$roomnameidenfier = $coursecommunication->get_room_name();
if (empty($course->$roomnameidenfier)) {
$course->$roomnameidenfier = $course->fullname ?? get_course($course->id)->fullname;
}
}
// List of enrolled users for course communication.
$enrolledusers = self::get_enrolled_users_for_course(course: $course);
// Check for group mode, we will have to get the course data again as the group info is not always in the object.
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
// If group mode is disabled, get the communication information for creating room for a course.
if ((int)$groupmode === NOGROUPS) {
// Remove all the members from active group rooms if there is any.
$coursegroups = groups_get_all_groups(courseid: $course->id);
foreach ($coursegroups as $coursegroup) {
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
// Remove the members from the group room.
$communication->remove_all_members_from_room();
// Now delete the group room.
$communication->update_room(active: processor::PROVIDER_INACTIVE);
}
// Now create/update the course room.
$communication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $course->$roomnameidenfier,
users: $enrolledusers,
instanceimage: $courseimage,
);
} else {
// Update the group communication instances.
self::update_group_communication_instances_for_course(
course: $course,
provider: $provider,
);
// Remove all the members for the course room if instance available.
$communication = self::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
// Now handle the course communication according to the provider.
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $course->$roomnameidenfier,
users: $enrolledusers,
instanceimage: $courseimage,
queue: false,
);
// As the course is in group mode, make sure no users are in the course room.
$communication->reload();
$communication->remove_all_members_from_room();
}
}
/**
* Update the group communication instances.
*
* @param stdClass $course The course object.
* @param string $provider The provider name.
*/
public static function update_group_communication_instances_for_course(
stdClass $course,
string $provider,
): void {
$coursegroups = groups_get_all_groups(courseid: $course->id);
$coursecontext = \context_course::instance(courseid: $course->id);
$allaccessgroupusers = self::get_users_has_access_to_all_groups(
userids: self::get_enrolled_users_for_course(course: $course),
courseid: $course->id,
);
foreach ($coursegroups as $coursegroup) {
$groupusers = array_column(
groups_get_members(groupid: $coursegroup->id),
'id',
);
// Filter out users who are not active in this course.
$enrolledusers = self::get_enrolled_users_for_course(course: $course);
$groupuserstoadd = array_intersect($groupusers, $enrolledusers);
foreach ($allaccessgroupusers as $allaccessgroupuser) {
if (!in_array($allaccessgroupuser, $groupuserstoadd, true)) {
$groupuserstoadd[] = $allaccessgroupuser;
}
}
// Now create/update the group room.
$communication = self::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$roomnameidenfier = $provider . 'roomname';
$communicationroomname = self::format_group_room_name(
baseroomname: $course->$roomnameidenfier,
groupname: $coursegroup->name,
);
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $communicationroomname,
users: $groupuserstoadd,
);
}
}
/**
* Format a group communication room name with the following syntax: 'Group A (Course 1)'.
*
* @param string $baseroomname The base room name.
* @param string $groupname The group name.
*/
public static function format_group_room_name(
string $baseroomname,
string $groupname
): string {
return get_string('communicationgrouproomnameformat', 'core_communication', [
'groupname' => $groupname,
'baseroomname' => $baseroomname,
]);
}
}
+665
View File
@@ -0,0 +1,665 @@
<?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 context_course;
use core\hook\access\after_role_assigned;
use core\hook\access\after_role_unassigned;
use core_enrol\hook\before_enrol_instance_deleted;
use core_enrol\hook\after_enrol_instance_status_updated;
use core_enrol\hook\after_user_enrolled;
use core_enrol\hook\before_user_enrolment_updated;
use core_enrol\hook\before_user_enrolment_removed;
use core_course\hook\after_course_created;
use core_course\hook\before_course_deleted;
use core_course\hook\after_course_updated;
use core_group\hook\after_group_created;
use core_group\hook\after_group_deleted;
use core_group\hook\after_group_membership_added;
use core_group\hook\after_group_membership_removed;
use core_group\hook\after_group_updated;
use core_user\hook\before_user_deleted;
use core_user\hook\before_user_updated;
/**
* Hook listener for communication api.
*
* @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 hook_listener {
/**
* Get the course and group object for the group hook.
*
* @param mixed $hook The hook object.
* @return array
*/
protected static function get_group_and_course_data_for_group_hook(mixed $hook): array {
$group = $hook->groupinstance;
$course = helper::get_course(
courseid: $group->courseid,
);
return [
$group,
$course,
];
}
/**
* Communication api call to create room for a group if course has group mode enabled.
*
* @param after_group_created $hook The group created hook.
*/
public static function create_group_communication(
after_group_created $hook,
): void {
[$group, $course] = self::get_group_and_course_data_for_group_hook(
hook: $hook,
);
// Check if group mode enabled before handling the communication.
if (!helper::is_group_mode_enabled_for_course(course: $course)) {
return;
}
$coursecontext = \context_course::instance(courseid: $course->id);
// Get the course communication instance to set the provider.
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
// Check we have communication correctly set up before proceeding.
if ($coursecommunication->get_processor() === null) {
return;
}
$communication = api::load_by_instance(
context: $coursecontext,
component: constants::GROUP_COMMUNICATION_COMPONENT,
instancetype: constants::GROUP_COMMUNICATION_INSTANCETYPE,
instanceid: $group->id,
provider: $coursecommunication->get_provider(),
);
$communicationroomname = helper::format_group_room_name(
baseroomname: $coursecommunication->get_room_name(),
groupname: $group->name,
);
$communication->create_and_configure_room(
communicationroomname: $communicationroomname,
instance: $course,
);
// As it's a new group, we need to add the users with all access group role to the room.
$enrolledusers = helper::get_enrolled_users_for_course(course: $course);
$userstoadd = helper::get_users_has_access_to_all_groups(
userids: $enrolledusers,
courseid: $course->id,
);
$communication->add_members_to_room(
userids: $userstoadd,
queue: false,
);
}
/**
* Communication api call to update room for a group if course has group mode enabled.
*
* @param after_group_updated $hook The group updated hook.
*/
public static function update_group_communication(
after_group_updated $hook,
): void {
[$group, $course] = self::get_group_and_course_data_for_group_hook(
hook: $hook,
);
// Check if group mode enabled before handling the communication.
if (!helper::is_group_mode_enabled_for_course(course: $course)) {
return;
}
$coursecontext = \context_course::instance(courseid: $course->id);
$communication = helper::load_by_group(
groupid: $group->id,
context: $coursecontext,
);
// Get the course communication instance so we can extract the base room name.
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$communicationroomname = helper::format_group_room_name(
baseroomname: $coursecommunication->get_room_name(),
groupname: $group->name,
);
// If the name didn't change, then we don't need to update the room.
if ($communicationroomname === $communication->get_room_name()) {
return;
}
$communication->update_room(
active: processor::PROVIDER_ACTIVE,
communicationroomname: $communicationroomname,
instance: $course,
);
}
/**
* Delete the communication room for a group if course has group mode enabled.
*
* @param after_group_deleted $hook The group deleted hook.
*/
public static function delete_group_communication(
after_group_deleted $hook
): void {
[$group, $course] = self::get_group_and_course_data_for_group_hook(
hook: $hook,
);
// Check if group mode enabled before handling the communication.
if (!helper::is_group_mode_enabled_for_course(course: $course)) {
return;
}
$context = context_course::instance($course->id);
$communication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
$communication->delete_room();
}
/**
* Add members to group room when a new member is added to the group.
*
* @param after_group_membership_added $hook The group membership added hook.
*/
public static function add_members_to_group_room(
after_group_membership_added $hook,
): void {
[$group, $course] = self::get_group_and_course_data_for_group_hook(
hook: $hook,
);
// Check if group mode enabled before handling the communication.
if (!helper::is_group_mode_enabled_for_course(course: $course)) {
return;
}
$context = context_course::instance($course->id);
$communication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
// Filter out users who are not active in this course.
$enrolledusers = helper::get_enrolled_users_for_course($course, true);
$userids = array_intersect($hook->userids, $enrolledusers);
$communication->add_members_to_room(
userids: $userids,
);
}
/**
* Remove members from the room when a member is removed from group room.
*
* @param after_group_membership_removed $hook The group membership removed hook.
*/
public static function remove_members_from_group_room(
after_group_membership_removed $hook,
): void {
[$group, $course] = self::get_group_and_course_data_for_group_hook(
hook: $hook,
);
// Check if group mode enabled before handling the communication.
if (!helper::is_group_mode_enabled_for_course(course: $course)) {
return;
}
$context = context_course::instance($course->id);
$communication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
$communication->remove_members_from_room(
userids: $hook->userids,
);
}
/**
* Create course communication instance.
*
* @param after_course_created $hook The course created hook.
*/
public static function create_course_communication(
after_course_created $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$course = $hook->course;
// Check for default provider config setting.
$defaultprovider = get_config(
plugin: 'moodlecourse',
name: 'coursecommunicationprovider',
);
$provider = $course->selectedcommunication ?? $defaultprovider;
if (empty($provider) || $provider === processor::PROVIDER_NONE) {
return;
}
// Check for group mode, we will have to get the course data again as the group info is not always in the object.
$createcourseroom = true;
$creategrouprooms = false;
$coursedata = get_course(courseid: $course->id);
$groupmode = $course->groupmode ?? $coursedata->groupmode;
if ((int)$groupmode !== NOGROUPS) {
$createcourseroom = false;
$creategrouprooms = true;
}
// Prepare the communication api data.
$courseimage = course_get_courseimage(course: $course);
$communicationroomname = !empty($course->communicationroomname) ? $course->communicationroomname : $coursedata->fullname;
$coursecontext = \context_course::instance(courseid: $course->id);
// Communication api call for course communication.
$communication = \core_communication\api::load_by_instance(
context: $coursecontext,
component: constants::COURSE_COMMUNICATION_COMPONENT,
instancetype: constants::COURSE_COMMUNICATION_INSTANCETYPE,
instanceid: $course->id,
provider: $provider,
);
$communication->create_and_configure_room(
communicationroomname: $communicationroomname,
avatar: $courseimage,
instance: $course,
queue: $createcourseroom,
);
// Communication api call for group communication.
if ($creategrouprooms) {
helper::update_group_communication_instances_for_course(
course: $course,
provider: $provider,
);
} else {
$enrolledusers = helper::get_enrolled_users_for_course(course: $course);
$communication->add_members_to_room(
userids: $enrolledusers,
queue: false,
);
}
}
/**
* Update the course communication instance.
*
* @param after_course_updated $hook The course updated hook.
*/
public static function update_course_communication(
after_course_updated $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$course = $hook->course;
$oldcourse = $hook->oldcourse;
$changeincoursecat = $hook->changeincoursecat;
$groupmode = $course->groupmode ?? get_course($course->id)->groupmode;
if ($changeincoursecat || $groupmode !== $oldcourse->groupmode) {
helper::update_course_communication_instance(
course: $course,
changesincoursecat: $changeincoursecat,
);
}
}
/**
* Delete course communication data and remove members.
* Course can have communication data if it is a group or a course.
* This action is important to perform even if the experimental feature is disabled.
*
* @param before_course_deleted $hook The course deleted hook.
*/
public static function delete_course_communication(
before_course_deleted $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$course = $hook->course;
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
$coursecontext = \context_course::instance(courseid: $course->id);
// If group mode is not set then just handle the course communication room.
if ((int)$groupmode === NOGROUPS) {
$communication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$communication->delete_room();
} else {
// If group mode is set then handle the group communication rooms.
$coursegroups = groups_get_all_groups(courseid: $course->id);
foreach ($coursegroups as $coursegroup) {
$communication = helper::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
$communication->delete_room();
}
}
}
/**
* Update the room membership for the user updates.
*
* @param before_user_updated $hook The user updated hook.
*/
public static function update_user_room_memberships(
before_user_updated $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$user = $hook->user;
$currentuserrecord = $hook->currentuserdata;
// Get the user courses.
$usercourses = enrol_get_users_courses(userid: $user->id);
// If the user is suspended then remove the user from all the rooms.
// Otherwise add the user to all the rooms for the courses the user enrolled in.
if (!empty($currentuserrecord) && isset($user->suspended) && $currentuserrecord->suspended !== $user->suspended) {
// Decide the action for the communication api for the user.
$memberaction = ($user->suspended === 0) ? 'add_members_to_room' : 'remove_members_from_room';
foreach ($usercourses as $usercourse) {
helper::update_course_communication_room_membership(
course: $usercourse,
userids: [$user->id],
memberaction: $memberaction,
);
}
}
}
/**
* Delete all room memberships for a user.
*
* @param before_user_deleted $hook The user deleted hook.
*/
public static function delete_user_room_memberships(
before_user_deleted $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$user = $hook->user;
foreach (enrol_get_users_courses(userid: $user->id) as $course) {
$groupmode = $course->groupmode ?? get_course(courseid: $course->id)->groupmode;
$coursecontext = \context_course::instance(courseid: $course->id);
if ((int)$groupmode === NOGROUPS) {
$communication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
if ($communication->get_processor() !== null) {
$communication->get_room_user_provider()->remove_members_from_room(userids: [$user->id]);
$communication->get_processor()->delete_instance_user_mapping(userids: [$user->id]);
}
} else {
// If group mode is set then handle the group communication rooms.
$coursegroups = groups_get_all_groups(courseid: $course->id);
foreach ($coursegroups as $coursegroup) {
$communication = helper::load_by_group(
groupid: $coursegroup->id,
context: $coursecontext,
);
if ($communication->get_processor() !== null) {
$communication->get_room_user_provider()->remove_members_from_room(userids: [$user->id]);
$communication->get_processor()->delete_instance_user_mapping(userids: [$user->id]);
}
}
}
}
}
/**
* Update the room membership of the user for role assigned in a course.
*
* @param after_role_assigned|after_role_unassigned $hook
*/
public static function update_user_membership_for_role_changes(
after_role_assigned|after_role_unassigned $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$context = $hook->context;
if ($coursecontext = $context->get_course_context(strict: false)) {
helper::update_course_communication_room_membership(
course: get_course(courseid: $coursecontext->instanceid),
userids: [$hook->userid],
memberaction: 'update_room_membership',
);
}
}
/**
* Update the communication memberships for enrol status change.
*
* @param after_enrol_instance_status_updated $hook The enrol status updated hook.
*/
public static function update_communication_memberships_for_enrol_status_change(
after_enrol_instance_status_updated $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$enrolinstance = $hook->enrolinstance;
// No need to do anything for guest instances.
if ($enrolinstance->enrol === 'guest') {
return;
}
$newstatus = $hook->newstatus;
// Check if a valid status is given.
if (
$newstatus !== ENROL_INSTANCE_ENABLED ||
$newstatus !== ENROL_INSTANCE_DISABLED
) {
return;
}
// Check if the status provided is valid.
switch ($newstatus) {
case ENROL_INSTANCE_ENABLED:
$action = 'add_members_to_room';
break;
case ENROL_INSTANCE_DISABLED:
$action = 'remove_members_from_room';
break;
default:
return;
}
global $DB;
$instanceusers = $DB->get_records(
table: 'user_enrolments',
conditions: ['enrolid' => $enrolinstance->id, 'status' => ENROL_USER_ACTIVE],
);
$enrolledusers = array_column($instanceusers, 'userid');
helper::update_course_communication_room_membership(
course: get_course(courseid: $enrolinstance->courseid),
userids: $enrolledusers,
memberaction: $action,
);
}
/**
* Remove the communication instance memberships when an enrolment instance is deleted.
*
* @param before_enrol_instance_deleted $hook The enrol instance deleted hook.
*/
public static function remove_communication_memberships_for_enrol_instance_deletion(
before_enrol_instance_deleted $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$enrolinstance = $hook->enrolinstance;
// No need to do anything for guest instances.
if ($enrolinstance->enrol === 'guest') {
return;
}
global $DB;
$instanceusers = $DB->get_records(
table: 'user_enrolments',
conditions: ['enrolid' => $enrolinstance->id, 'status' => ENROL_USER_ACTIVE],
);
$enrolledusers = array_column($instanceusers, 'userid');
helper::update_course_communication_room_membership(
course: get_course(courseid: $enrolinstance->courseid),
userids: $enrolledusers,
memberaction: 'remove_members_from_room',
);
}
/**
* Add communication instance membership for an enrolled user.
*
* @param after_user_enrolled $hook The user enrolled hook.
*/
public static function add_communication_membership_for_enrolled_user(
after_user_enrolled $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$enrolinstance = $hook->enrolinstance;
// No need to do anything for guest instances.
if ($enrolinstance->enrol === 'guest') {
return;
}
helper::update_course_communication_room_membership(
course: get_course($enrolinstance->courseid),
userids: [$hook->get_userid()],
memberaction: 'add_members_to_room',
);
}
/**
* Update the communication instance membership for the user enrolment updates.
*
* @param before_user_enrolment_updated $hook The user enrolment updated hook.
*/
public static function update_communication_membership_for_updated_user_enrolment(
before_user_enrolment_updated $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$enrolinstance = $hook->enrolinstance;
// No need to do anything for guest instances.
if ($enrolinstance->enrol === 'guest') {
return;
}
$userenrolmentinstance = $hook->userenrolmentinstance;
$statusmodified = $hook->statusmodified;
$timeendmodified = $hook->timeendmodified;
if (
($statusmodified && ((int) $userenrolmentinstance->status === 1)) ||
($timeendmodified && $userenrolmentinstance->timeend !== 0 && (time() > $userenrolmentinstance->timeend))
) {
$action = 'remove_members_from_room';
} else {
$action = 'add_members_to_room';
}
helper::update_course_communication_room_membership(
course: get_course($enrolinstance->courseid),
userids: [$hook->get_userid()],
memberaction: $action,
);
}
/**
* Remove communication instance membership for an enrolled user.
*
* @param before_user_enrolment_removed $hook The user unenrolled hook.
*/
public static function remove_communication_membership_for_unenrolled_user(
before_user_enrolment_removed $hook,
): void {
// If the communication subsystem is not enabled then just ignore.
if (!api::is_available()) {
return;
}
$enrolinstance = $hook->enrolinstance;
// No need to do anything for guest instances.
if ($enrolinstance->enrol === 'guest') {
return;
}
helper::update_course_communication_room_membership(
course: get_course($enrolinstance->courseid),
userids: [$hook->get_userid()],
memberaction: 'remove_members_from_room',
);
}
}
@@ -0,0 +1,70 @@
<?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\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\userlist;
/**
* Privacy Subsystem for core_communication implementing null_provider.
*
* @package core_communication
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
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', [
'commid' => 'privacy:metadata:communication_user:commid',
'userid' => 'privacy:metadata:communication_user:userid',
'synced' => 'privacy:metadata:communication_user:synced',
], 'privacy:metadata:communication_user');
return $collection;
}
public static function get_contexts_for_userid(int $userid): contextlist {
return new contextlist();
}
public static function export_user_data(approved_contextlist $contextlist) {
// None of the core communication tables should be exported.
}
public static function delete_data_for_all_users_in_context(\context $context) {
// None of the data from these tables should be deleted.
}
public static function delete_data_for_user(approved_contextlist $contextlist) {
// None of the data from these tables should be deleted.
}
public static function get_users_in_context(userlist $userlist) {
// Don't add any users.
}
public static function delete_data_for_users(approved_userlist $userlist) {
// None of the data from these tables should be deleted.
}
}
+763
View File
@@ -0,0 +1,763 @@
<?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 core\context;
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 communication_provider|room_chat_provider|room_user_provider|synchronise_provider|user_provider|null The provider class
*/
private communication_provider|user_provider|room_chat_provider|room_user_provider|synchronise_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 context $context The context of the item for the 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(
context $context,
string $provider,
int $instanceid,
string $component,
string $instancetype,
string $roomname,
): ?self {
global $DB;
if ($provider === self::PROVIDER_NONE) {
return null;
}
$record = (object) [
'contextid' => $context->id,
'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 $active Active state of the instance (processor::PROVIDER_ACTIVE or processor::PROVIDER_INACTIVE)
* @param null|string $roomname The room name
*/
public function update_instance(
?string $active = null,
?string $roomname = null,
): void {
global $DB;
if ($active !== null && in_array($active, [self::PROVIDER_ACTIVE, self::PROVIDER_INACTIVE])) {
$this->instancedata->active = $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]
);
}
/**
* 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.
*
* @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_available($record->provider)) {
return new self($record);
}
return null;
}
/**
* Load communication instance by instance id.
*
* @param context $context The context of the item for the instance
* @param string $component The component name
* @param string $instancetype The instance type
* @param int $instanceid The instance id
* @param string|null $provider The provider type - if null will load for this context's active provider.
* @return processor|null
*/
public static function load_by_instance(
context $context,
string $component,
string $instancetype,
int $instanceid,
?string $provider = null,
): ?self {
global $DB;
$params = [
'contextid' => $context->id,
'instanceid' => $instanceid,
'component' => $component,
'instancetype' => $instancetype,
];
if ($provider === null) {
// Fetch the active provider in this context.
$params['active'] = 1;
} else {
// Fetch a specific provider in this context (which may be inactive).
$params['provider'] = $provider;
}
$record = $DB->get_record('communication', $params);
if ($record && self::is_provider_available($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 the context of the communication instance.
*
* @return context
*/
public function get_context(): context {
return context::instance_by_id($this->get_context_id());
}
/**
* Get the context id of the communication instance.
*
* @return int
*/
public function get_context_id(): int {
return $this->instancedata->contextid;
}
/**
* Get communication instance type.
*
* @return string
*/
public function get_instance_type(): string {
return $this->instancedata->instancetype;
}
/**
* 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 {
return $this->instancedata->component;
}
/**
* Get communication provider type.
*
* @return string|null
*/
public function get_provider(): ?string {
return $this->instancedata->provider;
}
/**
* Get room name.
*
* @return string|null
*/
public function get_room_name(): ?string {
return $this->instancedata->roomname;
}
/**
* Get provider active status.
*
* @return int
*/
public function get_provider_status(): int {
return $this->instancedata->active;
}
/**
* 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 the provider after checking if it supports sync features.
*
* @return synchronise_provider
*/
public function get_sync_provider(): synchronise_provider {
$this->require_api_enabled();
$this->require_sync_provider_features();
return $this->provider;
}
/**
* Set provider specific form definition.
*
* @param string $provider The provider name
* @param \MoodleQuickForm $mform The moodle form
*/
public static function set_provider_specific_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');
}
}
/**
* Check if the provider supports sync features.
*
* @return bool whether the provider supports sync features or not
*/
public function supports_sync_provider_features(): bool {
return ($this->provider instanceof synchronise_provider);
}
/**
* Check if the provider supports sync features when required.
*/
public function require_sync_provider_features(): void {
if (!$this->supports_sync_provider_features()) {
throw new \coding_exception('sync 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 the communication provider enabled and configured, or disabled.
*
* @param string $provider provider component name
* @return bool
*/
public static function is_provider_available(string $provider): bool {
if (\core\plugininfo\communication::is_plugin_enabled($provider)) {
$providerclass = "{$provider}\\communication_feature";
return $providerclass::is_configured();
}
return false;
}
}
@@ -0,0 +1,50 @@
<?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;
/**
* Class communication_room_base to manage the room operations of communication providers.
*
* Every plugin that supports room operation must implement/extend this class in the plugin.
*
* @package core_communication
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @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.
*/
public function create_chat_room(): bool;
/**
* Update a provider room when a instance is updated.
*/
public function update_chat_room(): bool;
/**
* Delete a provider room when a instance is deleted.
*/
public function delete_chat_room(): bool;
/**
* Generate a room url if there is a room.
*
* @return string|null
*/
public function get_chat_room_url(): ?string;
}
@@ -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 core_communication;
/**
* Class communication_user_base to manage communication provider users.
*
* @package core_communication
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface room_user_provider {
/**
* Add members to communication room.
*
* @param array $userids The user ids to be added
*/
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.
*
* @param array $userids The user ids to be removed
*/
public function remove_members_from_room(array $userids): void;
}
@@ -0,0 +1,32 @@
<?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;
/**
* Interface synchronise_provider to check if the users and room is synced properly for the communication provider.
*
* @package core_communication
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface synchronise_provider {
/**
* Ensure the users are in sync with the communication provider.
*/
public function synchronise_room_members(): void;
}
@@ -0,0 +1,63 @@
<?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\task;
use core\task\adhoc_task;
use core_communication\processor;
/**
* Class add_members_to_room_task to add the task to add members to the room and execute the task to action the addition.
*
* @package core_communication
* @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>
* @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();
// 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()->add_members_to_room($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);
}
}
@@ -0,0 +1,74 @@
<?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\task;
use core\task\adhoc_task;
use core_communication\processor;
/**
* Class create_and_configure_room_task to add a task to create a room and execute the task to action the creation.
*
* this task will be queued by the communication api and will use the communication handler api to action the creation.
*
* @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 create_and_configure_room_task extends adhoc_task {
public function execute() {
$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;
}
if (!$communication->is_instance_active()) {
mtrace("Skipping room creation because the instance is not active");
return;
}
// If the room is created successfully, add members to the room if supported by the provider.
if ($communication->get_room_provider()->create_chat_room() && $communication->supports_user_features()) {
add_members_to_room_task::queue(
$communication
);
}
}
/**
* 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);
}
}
@@ -0,0 +1,71 @@
<?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\task;
use core\task\adhoc_task;
use core_communication\processor;
/**
* Class delete_room_task to add a task to delete a room and execute the task to action the deletion.
*
* this task will be queued by the communication api and will use the communication handler api to action the deletion.
*
* @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 delete_room_task extends adhoc_task {
public function execute() {
$data = $this->get_custom_data();
$communication = processor::load_by_id($data->id);
if ($communication === null) {
mtrace("Skipping room creation because the instance does not exist");
return;
}
// First remove the members from the room.
$communication->get_room_user_provider()->remove_members_from_room($communication->get_instance_userids(true, true));
// 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));
// Now delete the room.
if ($communication->get_room_provider()->delete_chat_room()) {
$communication->delete_instance();
}
}
/**
* 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);
}
}
@@ -0,0 +1,66 @@
<?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\task;
use core\task\adhoc_task;
use core_communication\processor;
/**
* Class remove_members_from_room to add the task to remove members to the room and execute the task to action the removal.
*
* @package core_communication
* @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>
* @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();
// 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()->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));
}
/**
* 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);
}
}
@@ -0,0 +1,63 @@
<?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\task;
use core\task\adhoc_task;
use core_communication\processor;
/**
* Class synchronise_provider_task to add a task to synchronise the provider and execute the task to action the synchronisation.
*
* @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 synchronise_provider_task extends adhoc_task {
public function execute() {
$data = $this->get_custom_data();
$communication = processor::load_by_id($data->id);
if ($communication === null) {
mtrace("Skipping provider sync because the instance does not exist");
return;
}
// Sync room members for the instance.
$communication->get_sync_provider()->synchronise_room_members();
}
/**
* 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);
}
}
@@ -0,0 +1,61 @@
<?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\task;
use core\task\scheduled_task;
use core_communication\api;
use core_communication\processor;
/**
* Class synchronise_providers to add a task to synchronise the providers and execute the task to action the synchronisation.
*
* @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 synchronise_providers_task extends scheduled_task {
public function get_name() {
return get_string('synchroniseproviders', 'core_communication');
}
public function execute() {
// Communication is not enabled? nothing to do.
if (!api::is_available()) {
return;
}
global $DB;
$communicationinstances = $DB->get_records(
table: 'communication',
conditions: ['active' => processor::PROVIDER_ACTIVE],
);
foreach ($communicationinstances as $communicationinstance) {
$communication = \core_communication\api::load_by_instance(
context: \context::instance_by_id($communicationinstance->contextid),
component: $communicationinstance->component,
instancetype: $communicationinstance->instancetype,
instanceid: $communicationinstance->instanceid,
);
$processor = $communication->get_processor();
if ($processor->supports_sync_provider_features()) {
$communication->sync_provider();
}
}
}
}
@@ -0,0 +1,63 @@
<?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\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 <safat.shahin@moodle.com>
* @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);
}
}
@@ -0,0 +1,64 @@
<?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\task;
use core\task\adhoc_task;
use core_communication\processor;
/**
* Class update_room_task to add a task to update a room and execute the task to action the update.
*
* this task will be queued by the communication api and will use the communication handler api to action the updates.
*
* @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 update_room_task extends adhoc_task {
public function execute() {
$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_provider()->update_chat_room();
}
/**
* 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);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?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;
/**
* Class communication_user_base to manage communication provider users.
*
* @package core_communication
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface user_provider {
/**
* Create members.
*
* @param array $userid The users ids to be created
*/
public function create_members(array $userid): void;
}
+102
View File
@@ -0,0 +1,102 @@
<?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/>.
/**
* Configure communication for a given instance.
*
* @package core_communication
* @copyright 2023 David Woloszyn <david.woloszyn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once('../config.php');
require_once('lib.php');
require_login();
$contextid = required_param('contextid', PARAM_INT);
$instanceid = required_param('instanceid', PARAM_INT);
$instancetype = required_param('instancetype', PARAM_TEXT);
$component = required_param('component', PARAM_COMPONENT);
$selectedcommunication = optional_param('selectedcommunication', null, PARAM_PLUGIN);
$context = \core\context::instance_by_id($contextid);
$instanceinfo = [
'contextid' => $context->id,
'instanceid' => $instanceid,
'instancetype' => $instancetype,
'component' => $component,
];
// Requires communication to be enabled.
if (!core_communication\api::is_available()) {
throw new \moodle_exception('communicationdisabled', 'communication');
}
// Attempt to load the communication instance with the provided params.
$communication = \core_communication\api::load_by_instance(
context: $context,
component: $component,
instancetype: $instancetype,
instanceid: $instanceid,
provider: $selectedcommunication,
);
// No communication, no way this form can be used.
if (!$communication) {
throw new \moodle_exception('nocommunicationinstance', 'communication');
}
// Set variables according to the component callback and use them on the page.
[$instance, $context, $heading, $returnurl] = component_callback(
$component,
'get_communication_instance_data',
[$instanceid]
);
// Set up the page.
$PAGE->set_context($context);
$PAGE->set_url('/communication/configure.php', $instanceinfo);
$PAGE->set_title(get_string('communication', 'communication'));
$PAGE->set_heading($heading);
$PAGE->add_body_class('limitedwidth');
// Append the instance data before passing to form object.
$instanceinfo['instancedata'] = $instance;
// Get our form definitions.
$form = new \core_communication\form\configure_form(
context: $context,
instanceid: $instanceinfo['instanceid'],
instancetype: $instanceinfo['instancetype'],
component: $instanceinfo['component'],
selectedcommunication: $selectedcommunication,
instancedata: $instanceinfo['instancedata'],
);
if ($form->is_cancelled()) {
redirect($returnurl);
} else if ($data = $form->get_data()) {
component_callback($component, 'update_communication_instance_data', [$data]);
redirect($returnurl);
}
// Display the page contents.
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('communication', 'communication'), 2);
$form->display();
echo $OUTPUT->footer();
+61
View File
@@ -0,0 +1,61 @@
<?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/>.
/**
* Callback and library methods for core communication.
*
* @package core_communication
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Serve the files from the core_communication file areas.
*
* @param stdClass $course the course object
* @param stdClass $cm the course module object
* @param context $context the context
* @param string $filearea the name of the file area
* @param array $args extra arguments (itemid, path)
* @param bool $forcedownload whether force download
* @param array $options additional options affecting the file serving
*/
function core_communication_pluginfile(
$course,
$cm,
$context,
$filearea,
$args,
$forcedownload,
array $options = []
): void {
if ($filearea !== 'avatar') {
return;
}
$itemid = array_shift($args);
$filename = array_pop($args);
// Retrieve the file from the Files API.
$fs = get_file_storage();
$file = $fs->get_file($context->id, 'core_communication', $filearea, $itemid, '/', $filename);
if (!$file) {
return;
}
send_file($file, $filename);
}
@@ -0,0 +1,181 @@
<?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_customlink;
use core_communication\processor;
/**
* class communication_feature to handle custom link specific actions.
*
* @package communication_customlink
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class communication_feature implements
\core_communication\communication_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';
/** @var \cache_application $cache The application cache for this provider. */
protected \cache_application $cache;
/**
* Load the communication provider for the communication API.
*
* @param processor $communication The communication processor object.
* @return communication_feature The communication provider object.
*/
public static function load_for_instance(processor $communication): self {
return new self($communication);
}
/**
* Constructor for communication provider.
*
* @param processor $communication The communication processor object.
*/
private function __construct(
private \core_communication\processor $communication,
) {
$this->cache = \cache::make('communication_customlink', 'customlink');
}
/**
* Create room - room existence managed externally, always return true.
*
* @return boolean
*/
public function create_chat_room(): bool {
return true;
}
/**
* Update room - room existence managed externally, always return true.
*
* @return boolean
*/
public function update_chat_room(): bool {
return true;
}
/**
* Delete room - room existence managed externally, always return true.
*
* @return boolean
*/
public function delete_chat_room(): bool {
return true;
}
/**
* Fetch the URL for this custom link provider.
*
* @return string|null The custom URL, or null if not found.
*/
public function get_chat_room_url(): ?string {
global $DB;
$commid = $this->communication->get_id();
$cachekey = "link_url_{$commid}";
// Attempt to fetch the room URL from the cache.
if ($url = $this->cache->get($cachekey)) {
return $url;
}
// If not found in the cache, fetch the URL from the database.
$url = $DB->get_field(
self::CUSTOMLINK_TABLE,
'url',
['commid' => $commid],
);
// Cache the URL.
$this->cache->set($cachekey, $url);
return $url;
}
public function save_form_data(\stdClass $instance): void {
if (empty($instance->customlinkurl)) {
return;
}
global $DB;
$commid = $this->communication->get_id();
$cachekey = "link_url_{$commid}";
$newrecord = new \stdClass();
$newrecord->url = $instance->customlinkurl;
$existingrecord = $DB->get_record(
self::CUSTOMLINK_TABLE,
['commid' => $commid],
'id, url'
);
if (!$existingrecord) {
// Create the record if it does not exist.
$newrecord->commid = $commid;
$DB->insert_record(self::CUSTOMLINK_TABLE, $newrecord);
} else if ($instance->customlinkurl !== $existingrecord->url) {
// Update record if the URL has changed.
$newrecord->id = $existingrecord->id;
$DB->update_record(self::CUSTOMLINK_TABLE, $newrecord);
} else {
// No change made.
return;
}
// Cache the new URL.
$this->cache->set($cachekey, $newrecord->url);
}
public function set_form_data(\stdClass $instance): void {
if (!empty($instance->id) && !empty($this->communication->get_id())) {
$instance->customlinkurl = $this->get_chat_room_url();
}
}
public static function set_form_definition(\MoodleQuickForm $mform): void {
// Custom link description for the communication provider.
$mform->insertElementBefore($mform->createElement(
'text',
'customlinkurl',
get_string('customlinkurl', 'communication_customlink'),
'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',
'',
get_string('customlinkurlinfo', 'communication_customlink'),
'addcommunicationoptionshere'
), 'addcommunicationoptionshere');
}
public static function is_configured(): bool {
return true;
}
}
@@ -0,0 +1,38 @@
<?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_customlink\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy Subsystem for communication_customlink implementing null_provider.
*
* @package communication_customlink
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @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.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,35 @@
<?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/>.
/**
* Defined caches used internally by the provider.
*
* @package communication_customlink
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
defined('MOODLE_INTERNAL') || die();
$definitions = [
'customlink' => [
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'simpledata' => true,
],
];
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="communication/provider/customlink/db" VERSION="20230826" COMMENT="Stores the link associated with a custom link communication instance."
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="communication_customlink" COMMENT="Stores the link associated with a custom link communication instance.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="commid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of the communication record"/>
<FIELD NAME="url" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="URL being linked to by the provider"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_commid" TYPE="foreign" FIELDS="commid" REFTABLE="communication" REFFIELDS="id" COMMENT="Foreign key for communication reference"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
@@ -0,0 +1,30 @@
<?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/>.
/**
* Strings for component communication_customlink, language 'en'.
*
* @package communication_customlink
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['cachedef_customlink'] = 'Custom link data';
$string['customlinkurl'] = 'Custom link URL';
$string['customlinkurl_help'] = 'Provide a link to an existing room from any communication service you would like to make available to participants - such as Microsoft Teams, Slack or Matrix.';
$string['customlinkurlinfo'] = 'The URL of an existing room already set up for this course.';
$string['pluginname'] = 'Custom link';
$string['privacy:metadata'] = 'Custom link communication plugin does not store any personal data.';
@@ -0,0 +1,86 @@
@communication @communication_customlink @javascript
Feature: Communication custom link
In order to facilitate easy access to an existing communication platform
As a teacher
I need to be able to make a custom communication link available in my course
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following config values are set as admin:
| enablecommunicationsubsystem | 1 |
Scenario: As a teacher I can configure a custom communication provider for my course
Given I am on the "Course 1" "Course" page logged in as "teacher1"
And "Chat to course participants" "button" should not be visible
When I navigate to "Communication" in current page administration
And the "Provider" select box should contain "Custom link"
And I should not see "Custom link URL"
And I select "Custom link" from the "Provider" singleselect
And I should see "Custom link URL"
And I set the following fields to these values:
| communication_customlinkroomname | Test URL |
| customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php |
And I press "Save changes"
Then "Chat to course participants" "button" should be visible
And I click on "Chat to course participants" "button"
# Check the link hits the expected destination.
And I switch to a second window
And I should see "Example messaging service - teacher1" in the "region-main" "region"
And I close all opened windows
# Ensure any communication subsystem tasks have no impact on availability.
And I run all adhoc tasks
And I am on the "Course 1" course page
And "Chat to course participants" "button" should be visible
And I click on "Chat to course participants" "button"
And I switch to a second window
And I should see "Example messaging service - teacher1" in the "region-main" "region"
And I close all opened windows
And I log out
# Confirm student also has access to the custom link.
And I am on the "Course 1" "Course" page logged in as "student1"
And "Chat to course participants" "button" should be visible
And I click on "Chat to course participants" "button"
And I switch to a second window
And I should see "Example messaging service - student1" in the "region-main" "region"
Scenario: As a teacher I can disable and re-enable a custom communication provider for my course
Given I am on the "Course 1" "Course" page logged in as "teacher1"
And "Chat to course participants" "button" should not be visible
When I navigate to "Communication" in current page administration
And I select "Custom link" from the "Provider" singleselect
And I set the following fields to these values:
| communication_customlinkroomname | Test URL |
| customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php |
And I press "Save changes"
And "Chat to course participants" "button" should be visible
And I run all adhoc tasks
And I navigate to "Communication" in current page administration
And I select "None" from the "Provider" singleselect
And I press "Save changes"
And "Chat to course participants" "button" should not be visible
And I run all adhoc tasks
And I am on the "Course 1" course page
And "Chat to course participants" "button" should not be visible
And I navigate to "Communication" in current page administration
And I select "Custom link" from the "Provider" singleselect
And I set the following fields to these values:
| communication_customlinkroomname | Test URL |
| customlinkurl | #wwwroot#/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php |
And I press "Save changes"
And "Chat to course participants" "button" should be visible
And I run all adhoc tasks
And I am on the "Course 1" course page
And "Chat to course participants" "button" should be visible
And I click on "Chat to course participants" "button"
And I switch to a second window
And I should see "Example messaging service - teacher1" in the "region-main" "region"
@@ -0,0 +1,40 @@
<?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/>.
/**
* A page which can be used to represent a messaging service while testing the custom link communication provider.
*
* The current Moodle user's username is listed in the heading to make it easier to confirm the page has been
* opened by the expected user.
*
* @package communication_customlink
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../../../config.php');
defined('BEHAT_SITE_RUNNING') || die();
global $OUTPUT, $PAGE, $USER;
$PAGE->set_url('/communication/provider/customlink/tests/behat/fixtures/custom_link_test_page.php');
require_login();
$PAGE->set_context(core\context\system::instance());
echo $OUTPUT->header();
echo "<h2>Example messaging service - {$USER->username}</h2>";
echo "<p>Imagine this is a wonderful messaging service being accessed directly from a link in Moodle!</p>";
echo $OUTPUT->footer();
@@ -0,0 +1,144 @@
<?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_customlink;
use core_communication\processor;
use core_communication\communication_test_helper_trait;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../tests/communication_test_helper_trait.php');
/**
* Class communication_feature_test to test the custom link features implemented using the core interfaces.
*
* @package communication_customlink
* @category test
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \communication_customlink\communication_feature
*/
class communication_feature_test extends \advanced_testcase {
use communication_test_helper_trait;
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setup_communication_configs();
}
/**
* Test create, update and delete chat room.
*
* @covers ::load_for_instance
*/
public function test_load_for_instance(): void {
$communicationprocessor = $this->get_test_communication_processor();
$instance = communication_feature::load_for_instance($communicationprocessor);
$this->assertInstanceOf('communication_customlink\communication_feature', $instance);
}
/**
* Test create, update and delete chat room.
*
* @covers ::create_chat_room
* @covers ::update_chat_room
* @covers ::delete_chat_room
*/
public function test_create_update_delete_chat_room(): void {
$communicationprocessor = $this->get_test_communication_processor();
// Create, update and delete room should always return true because this provider contains
// a link to a room, but does not manage the existence of the room.
$createroomresult = $communicationprocessor->get_room_provider()->create_chat_room();
$updateroomresult = $communicationprocessor->get_room_provider()->update_chat_room();
$deleteroomresult = $communicationprocessor->get_room_provider()->delete_chat_room();
$this->assertTrue($createroomresult);
$this->assertTrue($updateroomresult);
$this->assertTrue($deleteroomresult);
}
/**
* Test save form data with provider's custom field and fetching with get_chat_room_url().
*
* @covers ::save_form_data
* @covers ::get_chat_room_url
*/
public function test_save_form_data(): void {
$communicationprocessor = $this->get_test_communication_processor();
$customlinkurl = 'https://moodle.org/message/index.php';
$formdatainstance = (object) ['customlinkurl' => $customlinkurl];
// Test the custom link URL is saved and can be retrieved as expected.
$communicationprocessor->get_form_provider()->save_form_data($formdatainstance);
$fetchedurl = $communicationprocessor->get_room_provider()->get_chat_room_url();
$this->assertEquals($customlinkurl, $fetchedurl);
// Test with empty customlinkurl.
$customlinkurlempty = '';
$formdatainstance = (object) ['customlinkurl' => $customlinkurlempty];
$communicationprocessor->get_form_provider()->save_form_data($formdatainstance);
$fetchedurl = $communicationprocessor->get_room_provider()->get_chat_room_url();
// It should not update the url to an empty one.
$this->assertEquals($customlinkurl, $fetchedurl);
// Test with null customlinkurl.
$customlinkurlempty = null;
$formdatainstance = (object) ['customlinkurl' => $customlinkurlempty];
$communicationprocessor->get_form_provider()->save_form_data($formdatainstance);
$fetchedurl = $communicationprocessor->get_room_provider()->get_chat_room_url();
// It should not update the url to a null one.
$this->assertEquals($customlinkurl, $fetchedurl);
}
/**
* Create a test custom link communication processor object.
*
* @return processor
*/
protected function get_test_communication_processor(): processor {
$course = $this->getDataGenerator()->create_course();
$instanceid = $course->id;
$context = \core\context\system::instance();
$component = 'core_course';
$instancetype = 'coursecommunication';
$selectedcommunication = 'communication_customlink';
$communicationroomname = 'communicationroom';
$communicationprocessor = processor::create_instance(
$context,
$selectedcommunication,
$instanceid,
$component,
$instancetype,
$communicationroomname,
);
return $communicationprocessor;
}
/**
* Test if the selected provider is configured.
*
* @covers ::is_configured
*/
public function test_is_configured(): void {
$communicationprocessor = $this->get_test_communication_processor();
$this->assertTrue($communicationprocessor->get_form_provider()->is_configured());
}
}
@@ -0,0 +1,30 @@
<?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/>.
/**
* Version information for communication_customlink.
*
* @package communication_customlink
* @copyright 2023 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'communication_customlink';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_ALPHA;
@@ -0,0 +1,792 @@
<?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;
use communication_matrix\local\spec\features\matrix\{
create_room_v3 as create_room_feature,
get_room_members_v3 as get_room_members_feature,
remove_member_from_room_v3 as remove_member_from_room_feature,
update_room_avatar_v3 as update_room_avatar_feature,
update_room_name_v3 as update_room_name_feature,
update_room_topic_v3 as update_room_topic_feature,
upload_content_v3 as upload_content_feature,
media_create_v1 as media_create_feature,
};
use communication_matrix\local\spec\features\synapse\{
create_user_v2 as create_user_feature,
get_room_info_v1 as get_room_info_feature,
get_user_info_v2 as get_user_info_feature,
invite_member_to_room_v1 as invite_member_to_room_feature,
};
use core_communication\processor;
use stdClass;
use GuzzleHttp\Psr7\Response;
/**
* class communication_feature to handle matrix specific actions.
*
* @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 communication_feature implements
\core_communication\communication_provider,
\core_communication\form_provider,
\core_communication\room_chat_provider,
\core_communication\room_user_provider,
\core_communication\synchronise_provider,
\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 */
protected ?string $homeserverurl = null;
/** @var string The URI of the Matrix web client */
protected string $webclienturl;
/** @var \communication_matrix\local\spec\v1p1|null The Matrix API processor */
protected ?matrix_client $matrixapi;
/**
* Load the communication provider for the communication api.
*
* @param processor $communication The communication processor object
* @return communication_feature The communication provider object
*/
public static function load_for_instance(processor $communication): self {
return new self($communication);
}
/**
* Reload the room information.
* This may be necessary after a room has been created or updated via the adhoc task.
* This is primarily intended for use in unit testing, but may have real world cases too.
*/
public function reload(): void {
$this->room = null;
$this->processor = processor::load_by_id($this->processor->get_id());
}
/**
* Constructor for communication provider to initialize necessary objects for api cals etc..
*
* @param processor $processor The communication processor object
*/
private function __construct(
private \core_communication\processor $processor,
) {
$this->homeserverurl = get_config('communication_matrix', 'matrixhomeserverurl');
$this->webclienturl = get_config('communication_matrix', 'matrixelementurl');
if ($processor::is_provider_available('communication_matrix')) {
// Generate the API instance.
$this->matrixapi = matrix_client::instance(
serverurl: $this->homeserverurl,
accesstoken: get_config('communication_matrix', 'matrixaccesstoken'),
);
}
}
/**
* Check whether the room configuration has been created yet.
*
* @return bool
*/
protected function room_exists(): bool {
return (bool) $this->get_room_configuration();
}
/**
* Whether the room exists on the remote server.
* This does not involve a remote call, but checks whether Moodle is aware of the room id.
* @return bool
*/
protected function remote_room_exists(): bool {
$room = $this->get_room_configuration();
return $room && ($room->get_room_id() !== null);
}
/**
* Get the stored room configuration.
* @return null|matrix_room
*/
public function get_room_configuration(): ?matrix_room {
$this->room = matrix_room::load_by_processor_id($this->processor->get_id());
return $this->room;
}
/**
* Return the current room id.
*
* @return string|null
*/
public function get_room_id(): ?string {
return $this->get_room_configuration()?->get_room_id();
}
/**
* Create members.
*
* @param array $userids The Moodle user ids to create
*/
public function create_members(array $userids): void {
$addedmembers = [];
// This API requiures the create_user feature.
$this->matrixapi->require_feature(create_user_feature::class);
foreach ($userids as $userid) {
$user = \core_user::get_user($userid);
$userfullname = fullname($user);
// Proceed if we have a user's full name and email to work with.
if (!empty($user->email) && !empty($userfullname)) {
$qualifiedmuid = matrix_user_manager::get_formatted_matrix_userid($user->username);
// First create user in matrix.
$response = $this->matrixapi->create_user(
userid: $qualifiedmuid,
displayname: $userfullname,
threepids: [(object) [
'medium' => 'email',
'address' => $user->email,
], ],
externalids: [],
);
$body = json_decode($response->getBody());
if (!empty($matrixuserid = $body->name)) {
// Then create matrix user id in moodle.
matrix_user_manager::set_matrix_userid_in_moodle($userid, $qualifiedmuid);
if ($this->add_registered_matrix_user_to_room($matrixuserid)) {
$addedmembers[] = $userid;
}
}
}
}
// 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 {
// Filter out any users that are not room members yet.
$response = $this->matrixapi->get_room_members(
roomid: $this->get_room_id(),
);
$body = self::get_body($response);
if (isset($body->joined)) {
foreach ($userids as $key => $userid) {
$matrixuserid = matrix_user_manager::get_matrixid_from_moodle(
userid: $userid,
);
if (!array_key_exists($matrixuserid, (array) $body->joined)) {
unset($userids[$key]);
}
}
}
$this->set_matrix_power_levels();
// Mark the users as synced for the updated members.
$this->processor->mark_users_as_synced($userids);
}
/**
* Add members to a room.
*
* @param array $userids The user ids to add
*/
public function add_members_to_room(array $userids): void {
$unregisteredmembers = [];
$addedmembers = [];
foreach ($userids as $userid) {
$matrixuserid = matrix_user_manager::get_matrixid_from_moodle($userid);
if ($matrixuserid && $this->check_user_exists($matrixuserid)) {
if ($this->add_registered_matrix_user_to_room($matrixuserid)) {
$addedmembers[] = $userid;
}
} else {
$unregisteredmembers[] = $userid;
}
}
// 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);
// Create Matrix users.
if (count($unregisteredmembers) > 0) {
$this->create_members($unregisteredmembers);
}
}
/**
* Adds the registered matrix user id to room.
*
* @param string $matrixuserid Registered matrix user id
*/
private function add_registered_matrix_user_to_room(string $matrixuserid): bool {
// Require the invite_member_to_room API feature.
$this->matrixapi->require_feature(invite_member_to_room_feature::class);
if (!$this->check_room_membership($matrixuserid)) {
$response = $this->matrixapi->invite_member_to_room(
roomid: $this->get_room_id(),
userid: $matrixuserid,
);
$body = self::get_body($response);
if (empty($body->room_id)) {
return false;
}
if ($body->room_id !== $this->get_room_id()) {
return false;
}
return true;
}
return false;
}
/**
* Remove members from a room.
*
* @param array $userids The Moodle user ids to remove
*/
public function remove_members_from_room(array $userids): void {
// This API requiures the remove_members_from_room feature.
$this->matrixapi->require_feature(remove_member_from_room_feature::class);
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);
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 (
$this->check_user_exists($matrixuserid) &&
$this->check_room_membership($matrixuserid)
) {
$this->matrixapi->remove_member_from_room(
roomid: $this->get_room_id(),
userid: $matrixuserid,
);
$membersremoved[] = $userid;
}
}
$this->processor->delete_instance_user_mapping($membersremoved);
}
/**
* Check if a user exists in Matrix.
* Use if user existence is needed before doing something else.
*
* @param string $matrixuserid The Matrix user id to check
* @return bool
*/
public function check_user_exists(string $matrixuserid): bool {
// This API requires the get_user_info feature.
$this->matrixapi->require_feature(get_user_info_feature::class);
$response = $this->matrixapi->get_user_info(
userid: $matrixuserid,
);
$body = self::get_body($response);
return isset($body->name);
}
/**
* Check if a user is a member of a room.
* Use if membership confirmation is needed before doing something else.
*
* @param string $matrixuserid The Matrix user id to check
* @return bool
*/
public function check_room_membership(string $matrixuserid): bool {
// This API requires the get_room_members feature.
$this->matrixapi->require_feature(get_room_members_feature::class);
$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.
return isset($body->joined) && array_key_exists($matrixuserid, (array) $body->joined);
}
/**
* Create a room based on the data in the communication instance.
*
* @return bool
*/
public function create_chat_room(): bool {
if ($this->remote_room_exists()) {
// A room already exists. Update it instead.
return $this->update_chat_room();
}
// This method requires the create_room API feature.
$this->matrixapi->require_feature(create_room_feature::class);
$room = $this->get_room_configuration();
$response = $this->matrixapi->create_room(
name: $this->processor->get_room_name(),
visibility: 'private',
preset: 'private_chat',
initialstate: [],
options: [
'topic' => $room->get_topic(),
],
);
$response = self::get_body($response);
if (empty($response->room_id)) {
throw new \moodle_exception(
'Unable to determine ID of matrix room',
);
}
// Update our record of the matrix room_id.
$room->update_room_record(
roomid: $response->room_id,
);
// Update the room avatar.
$this->update_room_avatar();
return true;
}
public function update_chat_room(): bool {
if (!$this->remote_room_exists()) {
// No room exists. Create it instead.
return $this->create_chat_room();
}
$this->matrixapi->require_features([
get_room_info_feature::class,
update_room_name_feature::class,
update_room_topic_feature::class,
]);
// Get room data.
$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(
roomid: $this->get_room_id(),
name: $this->processor->get_room_name(),
);
}
// Update the room topic if set.
$localroomdata = $this->get_room_configuration();
if ($remoteroomdata->topic !== $localroomdata->get_topic()) {
$this->matrixapi->update_room_topic(
roomid: $localroomdata->get_room_id(),
topic: $localroomdata->get_topic(),
);
}
// Update room avatar.
$this->update_room_avatar();
return true;
}
public function delete_chat_room(): bool {
$this->get_room_configuration()->delete_room_record();
$this->room = null;
return true;
}
/**
* Update the room avatar when an instance image is added or updated.
*/
public function update_room_avatar(): void {
// Both of the following features of the remote API are required.
$this->matrixapi->require_features([
upload_content_feature::class,
update_room_avatar_feature::class,
]);
// Check if we have an avatar that needs to be synced.
if ($this->processor->is_avatar_synced()) {
return;
}
$instanceimage = $this->processor->get_avatar();
$contenturi = null;
if ($this->matrixapi->implements_feature(media_create_feature::class)) {
// From version 1.7 we can fetch a mxc URI and use it before uploading the content.
if ($instanceimage) {
$response = $this->matrixapi->media_create();
$contenturi = self::get_body($response)->content_uri;
// Now update the room avatar.
$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(
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.
if ($instanceimage) {
// First upload the content.
$response = $this->matrixapi->upload_content($instanceimage);
$body = self::get_body($response);
$contenturi = $body->content_uri;
}
// Now update the room avatar.
$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.
if ($response->getReasonPhrase() === 'OK') {
$this->processor->set_avatar_synced_flag(true);
}
}
public function get_chat_room_url(): ?string {
if (!$this->get_room_id()) {
// We don't have a room id for this record.
return null;
}
return sprintf(
"%s#/room/%s",
$this->webclienturl,
$this->get_room_id(),
);
}
public function save_form_data(\stdClass $instance): void {
$matrixroomtopic = $instance->matrixroomtopic ?? null;
$room = $this->get_room_configuration();
if ($room) {
$room->update_room_record(
topic: $matrixroomtopic,
);
} else {
$this->room = matrix_room::create_room_record(
processorid: $this->processor->get_id(),
topic: $matrixroomtopic,
);
}
}
public function set_form_data(\stdClass $instance): void {
if (!empty($instance->id) && !empty($this->processor->get_id())) {
if ($this->room_exists()) {
$instance->matrixroomtopic = $this->get_room_configuration()->get_topic();
}
}
}
public static function set_form_definition(\MoodleQuickForm $mform): void {
// Room description for the communication provider.
$mform->insertElementBefore($mform->createElement(
'text',
'matrixroomtopic',
get_string('matrixroomtopic', 'communication_matrix'),
'maxlength="255" size="20"'
), 'addcommunicationoptionshere');
$mform->addHelpButton('matrixroomtopic', 'matrixroomtopic', 'communication_matrix');
$mform->setType('matrixroomtopic', PARAM_TEXT);
}
/**
* Get the body of a response as a stdClass.
*
* @param Response $response
* @return stdClass
*/
public static function get_body(Response $response): stdClass {
$body = $response->getBody();
return json_decode($body, false, 512, JSON_THROW_ON_ERROR);
}
/**
* 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) {
$muid = matrix_user_manager::get_matrixid_from_moodle($userid);
if (isset($newuserpowerlevels[$muid]) && $newuserpowerlevels[$muid] < matrix_constants::POWER_LEVEL_MAXIMUM) {
unset($newuserpowerlevels[$muid]);
}
}
}
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',
);
}
return $this->get_body($response);
}
/**
* 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 {
$powerlevel = matrix_constants::POWER_LEVEL_DEFAULT;
if (has_capability('communication/matrix:moderator', $this->processor->get_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;
}
/*
* Check if matrix settings are configured
*
* @return boolean
*/
public static function is_configured(): bool {
// Matrix communication settings.
$matrixhomeserverurl = get_config('communication_matrix', 'matrixhomeserverurl');
$matrixaccesstoken = get_config('communication_matrix', 'matrixaccesstoken');
$matrixelementurl = get_config('communication_matrix', 'matrixelementurl');
if (
!empty($matrixhomeserverurl) &&
!empty($matrixaccesstoken) &&
(PHPUNIT_TEST || defined('BEHAT_SITE_RUNNING') || !empty($matrixelementurl))
) {
return true;
}
return false;
}
public function synchronise_room_members(): void {
$this->set_matrix_power_levels();
}
}
@@ -0,0 +1,189 @@
<?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;
use communication_matrix\matrix_client;
use GuzzleHttp\Psr7\Request;
use OutOfRangeException;
/**
* A command to be sent to the Matrix server.
*
* This class is a wrapper around the PSR-7 Request Interface implementation provided by Guzzle.
*
* It takes a set of common parameters and configurations and turns them into a Request that can be called against a live server.
*
* @package communication_matrix
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class command extends Request {
/** @var array $command The raw command data */
/** @var array|null $params The parameters passed into the command */
/** @var bool $sendasjson Whether to send params as JSON */
/** @var bool $requireauthorization Whether authorization is required for this request */
/** @var bool $ignorehttperrors Whether to ignore HTTP Errors */
/** @var array $query Any query parameters to set on the URL */
/** @var array|null Any parameters not used in the URI which are to be passed to the server via body or query params */
protected array $remainingparams = [];
/**
* Create a new Command.
*
* @param matrix_client $client The URL for this method
* @param string $method (GET|POST|PUT|DELETE)
* @param string $endpoint The URL
* @param array $params Any parameters to pass
* @param array $query Any query parameters to set on the URL
* @param bool $ignorehttperrors Whether to ignore HTTP Errors
* @param bool $requireauthorization Whether authorization is required for this request
* @param bool $sendasjson Whether to send params as JSON
*/
public function __construct(
protected matrix_client $client,
string $method,
string $endpoint,
protected array $params = [],
protected array $query = [],
protected bool $ignorehttperrors = false,
protected bool $requireauthorization = true,
protected bool $sendasjson = true,
) {
foreach ($params as $name => $value) {
if ($name[0] === ':') {
if (preg_match("/{$name}\\b/", $endpoint) !== 1) {
throw new OutOfRangeException("Parameter not found in URL '{$name}'");
}
$endpoint = preg_replace("/{$name}\\b/", urlencode($value), $endpoint);
unset($params[$name]);
}
}
// Store the modified params.
$this->remainingparams = $params;
if (str_contains($endpoint, '/:')) {
throw new OutOfRangeException("URL contains untranslated parameters '{$endpoint}'");
}
// Process the required headers.
$headers = [
'Content-Type' => 'application/json',
];
if ($this->require_authorization()) {
$headers['Authorization'] = 'Bearer ' . $this->client->get_token();
}
// Construct the final request.
parent::__construct(
$method,
$this->get_url($endpoint),
$headers,
);
}
/**
* Get the URL of the endpoint on the server.
*
* @param string $endpoint
* @return string
*/
protected function get_url(string $endpoint): string {
return sprintf(
"%s/%s",
$this->client->get_server_url(),
$endpoint,
);
}
/**
* Get all parameters, including those set in the URL.
*
* @return array
*/
public function get_all_params(): array {
return $this->params;
}
/**
* Get the parameters provided to the command which are not used in the URL.
*
* These are typically passed to the server as query or body parameters instead.
*
* @return array
*/
public function get_remaining_params(): array {
return $this->remainingparams;
}
/**
* Get the Guzzle options to pass into the request.
*
* @return array
*/
public function get_options(): array {
$options = [];
if (count($this->query)) {
$options['query'] = $this->query;
}
if ($this->should_send_params_as_json()) {
$options['json'] = $this->get_remaining_params();
}
if ($this->should_ignore_http_errors()) {
$options['http_errors'] = false;
}
return $options;
}
/**
* Whether authorization is required.
*
* Based on the 'authorization' attribute set in a raw command.
*
* @return bool
*/
public function require_authorization(): bool {
return $this->requireauthorization;
}
/**
* Whether to ignore http errors on the response.
*
* Based on the 'ignore_http_errors' attribute set in a raw command.
*
* @return bool
*/
public function should_ignore_http_errors(): bool {
return $this->ignorehttperrors;
}
/**
* Whether to send remaining parameters as JSON.
*
* @return bool
*/
public function should_send_params_as_json(): bool {
return $this->sendasjson;
}
}
@@ -0,0 +1,78 @@
<?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 for room creation.
*
* https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3createroom
*
* @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 create_room_v3 {
/**
* Create a new room.
*
* @param string $name The room name
* @param null|string $visibility The room visibility
* @param null|string $preset The preset to use
* @param null|array $initialstate Initial state variables
* @param array $options Any additional options
* @return Response
*/
public function create_room(
string $name,
?string $visibility = null,
?string $preset = null,
?array $initialstate = null,
array $options = [],
): Response {
$params = [
'name' => $name,
];
if ($visibility !== null) {
$params['visibility'] = $visibility;
}
if ($preset !== null) {
$params['preset'] = $preset;
}
if ($initialstate !== null) {
$params['initial_state'] = $initialstate;
}
if (array_key_exists('topic', $options)) {
$params['topic'] = $options['topic'] ?? '';
}
return $this->execute(new command(
$this,
method: 'POST',
endpoint: '_matrix/client/v3/createRoom',
params: $params,
));
}
}
@@ -0,0 +1,52 @@
<?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 a list of room members.
*
* https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomidjoined_members
*
* @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_members_v3 {
/**
* Get a list of room members.
*
* @param string $roomid The room ID
* @return Response
*/
public function get_room_members(string $roomid): Response {
$params = [
':roomid' => $roomid,
];
return $this->execute(new command(
$this,
method: 'GET',
endpoint: '_matrix/client/v3/rooms/:roomid/joined_members',
params: $params,
));
}
}
@@ -0,0 +1,53 @@
<?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.
*
* https://spec.matrix.org/v1.1/client-server-api/#mroompower_levels
*
* @package communication_matrix
* @copyright 2024 David Woloszyn <david.woloszyn@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 get_room_power_levels_v3 {
/**
* Get a list of room members and their power levels.
*
* @param string $roomid The room ID
* @return Response
*/
public function get_room_power_levels(string $roomid): Response {
$params = [
':roomid' => $roomid,
];
return $this->execute(new command(
$this,
method: 'GET',
endpoint: '_matrix/client/r0/rooms/:roomid/state/m.room.power_levels',
params: $params,
));
}
}
@@ -0,0 +1,91 @@
<?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_from_sync(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,
));
}
}
@@ -0,0 +1,46 @@
<?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 create an mxc Media URI.
*
* https://spec.matrix.org/v1.1/client-server-api/#post_matrixmediav3upload
*
* @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 media_create_v1 {
/**
* Create a media URI.
*
* @return Response
*/
public function media_create(): Response {
return $this->execute(new command(
$this,
method: 'POST',
endpoint: '_matrix/media/v1/create',
));
}
}
@@ -0,0 +1,57 @@
<?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 remove a member from a room.
*
* https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidkick
*
* @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 remove_member_from_room_v3 {
/**
* Remove a member from a room.
*
* @param string $roomid The roomid to remove from
* @param string $userid The member to remove
* @return Response
*/
public function remove_member_from_room(
string $roomid,
string $userid,
): Response {
$params = [
':roomid' => $roomid,
'user_id' => $userid,
];
return $this->execute(new command(
$this,
method: 'POST',
endpoint: '_matrix/client/v3/rooms/:roomid/kick',
params: $params,
));
}
}
@@ -0,0 +1,58 @@
<?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 update a room avatar.
*
* https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
*
* @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 update_room_avatar_v3 {
/**
* Set the avatar for a room to the specified URL.
*
* @param string $roomid The roomid to set for
* @param null|string $avatarurl The mxc URL to use
* @return Response
*/
public function update_room_avatar(
string $roomid,
?string $avatarurl,
): Response {
$params = [
':roomid' => $roomid,
'url' => $avatarurl,
];
return $this->execute(new command(
$this,
method: 'PUT',
endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.avatar',
ignorehttperrors: true,
params: $params,
));
}
}
@@ -0,0 +1,54 @@
<?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 update a room name.
*
* https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
*
* @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 update_room_name_v3 {
/**
* Set the name for a room.
*
* @param string $roomid
* @param string $name
* @return Response
*/
public function update_room_name(string $roomid, string $name): Response {
$params = [
':roomid' => $roomid,
'name' => $name,
];
return $this->execute(new command(
$this,
method: 'PUT',
endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.name',
params: $params,
));
}
}
@@ -0,0 +1,77 @@
<?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,
));
}
}
@@ -0,0 +1,54 @@
<?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 update a room topic.
*
* https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
*
* @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 update_room_topic_v3 {
/**
* Set the topic for a room.
*
* @param string $roomid
* @param string $topic
* @return Response
*/
public function update_room_topic(string $roomid, string $topic): Response {
$params = [
':roomid' => $roomid,
'topic' => $topic,
];
return $this->execute(new command(
$this,
method: 'PUT',
endpoint: '_matrix/client/v3/rooms/:roomid/state/m.room.topic',
params: $params,
));
}
}
@@ -0,0 +1,83 @@
<?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;
use GuzzleHttp\Psr7\Utils;
/**
* Matrix API feature to upload content.
*
* https://spec.matrix.org/v1.1/client-server-api/#post_matrixmediav3upload
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <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 upload_content_v3 {
/**
* Upload the content in the matrix/synapse server.
*
* @param null|\stored_file $content The content to be uploaded
* @param null|string $mediaid The mediaid to associate a file with. Supported for v1.7 API an above only.
* @return Response
*/
public function upload_content(
?\stored_file $content,
?string $mediaid = null,
): Response {
$query = [];
if ($content) {
$query['filename'] = $content->get_filename();
}
if ($mediaid !== null) {
// Specification of the mediaid requires version 1.7 or above of the upload API.
// See https://spec.matrix.org/v1.7/client-server-api/#put_matrixmediav3uploadservernamemediaid.
$this->requires_version('1.7');
$command = new command(
$this,
method: 'PUT',
endpoint: '_matrix/media/v3/upload/:mediaid',
sendasjson: false,
query: $query,
params: [
':mediaid' => $mediaid,
],
);
} else {
$command = new command(
$this,
method: 'POST',
endpoint: '_matrix/media/v3/upload',
sendasjson: false,
query: $query,
);
}
if ($content) {
// Add the content-type, and header.
$command = $command->withHeader('Content-Type', $content->get_mimetype());
$command = $command->withBody(Utils::streamFor($content->get_content()));
}
return $this->execute($command);
}
}
@@ -0,0 +1,65 @@
<?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\synapse;
use communication_matrix\local\command;
use GuzzleHttp\Psr7\Response;
/**
* Synapse API feature for creating a user.
*
* https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <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 create_user_v2 {
/**
* Create a new user.
*
* @param string $userid The Matrix user id.
* @param string $displayname The visible name of the user
* @param array $threepids The third-party identifiers of the user.
* @param null|array $externalids
*/
public function create_user(
string $userid,
string $displayname,
array $threepids,
?array $externalids = null,
): Response {
$params = [
':userid' => $userid,
'displayname' => $displayname,
'threepids' => $threepids,
];
if ($externalids !== null) {
$params['externalids'] = $externalids;
}
return $this->execute(new command(
$this,
method: 'PUT',
endpoint: '_synapse/admin/v2/users/:userid',
params: $params,
));
}
}
@@ -0,0 +1,50 @@
<?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\synapse;
use communication_matrix\local\command;
use GuzzleHttp\Psr7\Response;
/**
* Synapse API feature for fetching room info.
*
* https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-details-api
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <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_info_v1 {
/**
* Get room info.
*
* @param string $roomid
* @return Response
*/
public function get_room_info(string $roomid): Response {
return $this->execute(new command(
$this,
method: 'GET',
endpoint: '_synapse/admin/v1/rooms/:roomid',
params: [
':roomid' => $roomid,
],
));
}
}
@@ -0,0 +1,51 @@
<?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\synapse;
use communication_matrix\local\command;
use GuzzleHttp\Psr7\Response;
/**
* Synapse API feature for fetching info about a user.
*
* https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#query-user-account
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <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_user_info_v2 {
/**
* Get user info.
*
* @param string $userid
* @return Response
*/
public function get_user_info(string $userid): Response {
return $this->execute(new command(
$this,
method: 'GET',
endpoint: '_synapse/admin/v2/users/:userid',
ignorehttperrors: true,
params: [
':userid' => $userid,
],
));
}
}
@@ -0,0 +1,56 @@
<?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\synapse;
use communication_matrix\local\command;
use GuzzleHttp\Psr7\Response;
/**
* Synapse API feature to invite a user into a room.
*
* https://matrix-org.github.io/synapse/latest/admin_api/room_membership.html#edit-room-membership-api
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <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 invite_member_to_room_v1 {
/**
* Join a user to a room.
*
* Note: This joins the user, and does not invite them.
*
* @param string $roomid
* @param string $userid
* @return Response
*/
public function invite_member_to_room(string $roomid, string $userid): Response {
$params = [
':roomid' => $roomid,
'user_id' => $userid,
];
return $this->execute(new command(
$this,
method: 'POST',
endpoint: '_synapse/admin/v1/join/:roomid',
params: $params,
));
}
}
@@ -0,0 +1,48 @@
<?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;
/**
* Matrix API to support version v1.1 of the Matrix specification.
*
* https://spec.matrix.org/v1.1/client-server-api/
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class v1p1 extends \communication_matrix\matrix_client {
// Use the standard matrix API for these features.
use features\matrix\create_room_v3;
use features\matrix\get_room_members_v3;
use features\matrix\remove_member_from_room_v3;
use features\matrix\update_room_avatar_v3;
use features\matrix\update_room_name_v3;
use features\matrix\update_room_topic_v3;
use features\matrix\upload_content_v3;
use features\matrix\update_room_power_levels_v3;
use features\matrix\get_room_powerlevels_from_sync_v3;
use features\matrix\get_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;
// User information and creation is a server-specific feature.
use features\synapse\get_user_info_v2;
use features\synapse\create_user_v2;
use features\synapse\get_room_info_v1;
}
@@ -0,0 +1,30 @@
<?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;
/**
* Matrix API to support version v1.2 of the Matrix specification.
*
* https://spec.matrix.org/v1.2/client-server-api/
* https://spec.matrix.org/v1.2/changelog/#api-changes
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class v1p2 extends v1p1 {
}
@@ -0,0 +1,30 @@
<?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;
/**
* Matrix API to support version v1.3 of the Matrix specification.
*
* https://spec.matrix.org/v1.3/client-server-api/
* https://spec.matrix.org/v1.3/changelog/#api-changes
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class v1p3 extends v1p2 {
}
@@ -0,0 +1,30 @@
<?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;
/**
* Matrix API to support version v1.4 of the Matrix specification.
*
* https://spec.matrix.org/v1.4/client-server-api/
* https://spec.matrix.org/v1.4/changelog/#api-changes
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class v1p4 extends v1p3 {
}
@@ -0,0 +1,30 @@
<?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;
/**
* Matrix API to support version v1.5 of the Matrix specification.
*
* https://spec.matrix.org/v1.5/client-server-api/
* https://spec.matrix.org/v1.5/changelog/#api-changes
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class v1p5 extends v1p4 {
}
@@ -0,0 +1,30 @@
<?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;
/**
* Matrix API to support version v1.6 of the Matrix specification.
*
* https://spec.matrix.org/v1.6/client-server-api/
* https://spec.matrix.org/v1.6/changelog/#api-changes
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class v1p6 extends v1p5 {
}
@@ -0,0 +1,34 @@
<?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;
/**
* Matrix API to support version v1.7 of the Matrix specification.
*
* https://spec.matrix.org/v1.7/client-server-api/
* https://spec.matrix.org/v1.7/changelog/#api-changes
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class v1p7 extends v1p6 {
// Note: A new Content Upload API was introduced.
// See details in the spec:
// https://github.com/matrix-org/matrix-spec-proposals/pull/2246.
use features\matrix\media_create_v1;
}
@@ -0,0 +1,348 @@
<?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;
use communication_matrix\local\command;
use core\http_client;
use DirectoryIterator;
use Exception;
use GuzzleHttp\Psr7\Response;
/**
* The abstract class for a versioned API client for Matrix.
*
* Matrix uses a versioned API, and a handshake occurs between the Client (Moodle) and server, to determine the APIs available.
*
* This client represents a version-less API client.
* Versions are implemented by combining the various features into a versionedclass.
* See v1p1 for example.
*
* @package communication_matrix
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class matrix_client {
/** @var string $serverurl The URL of the home server */
/** @var string $accesstoken The access token of the matrix server */
/** @var http_client|null The client to use */
protected static http_client|null $client = null;
/**
* Matrix events constructor to get the room id and refresh token usage if required.
*
* @param string $serverurl The URL of the API server
* @param string $accesstoken The admin access token
*/
protected function __construct(
protected string $serverurl,
protected string $accesstoken,
) {
}
/**
* Return the versioned instance of the API.
*
* @param string $serverurl The URL of the API server
* @param string $accesstoken The admin access token to use
* @return matrix_client|null
*/
public static function instance(
string $serverurl,
string $accesstoken,
): ?matrix_client {
// Fetch the list of supported API versions.
$clientversions = self::get_supported_versions();
// Fetch the supported versions from the server.
$serversupports = self::query_server_supports($serverurl);
if ($serversupports === null) {
// Unable to fetch the server versions.
return null;
}
$serverversions = $serversupports->versions;
// Calculate the intersections and sort to determine the highest combined version.
$versions = array_intersect($clientversions, $serverversions);
if (count($versions) === 0) {
// No versions in common.
throw new \moodle_exception('No supported Matrix API versions found.');
}
asort($versions);
$version = array_key_last($versions);
$classname = \communication_matrix\local\spec::class . '\\' . $version;
return new $classname(
$serverurl,
$accesstoken,
);
}
/**
* Determine if the API supports a feature.
*
* If an Array is provided, this will return true if any of the specified features is implemented.
*
* @param string[]|string $feature The feature to check. This is in the form of a namespaced class.
* @return bool
*/
public function implements_feature(array|string $feature): bool {
if (is_array($feature)) {
foreach ($feature as $thisfeature) {
if ($this->implements_feature($thisfeature)) {
return true;
}
}
// None of the features are implemented in this API version.
return false;
}
return in_array($feature, $this->get_supported_features());
}
/**
* Get a list of the features supported by this client.
*
* @return string[]
*/
public function get_supported_features(): array {
$features = [];
$class = static::class;
do {
$features = array_merge($features, class_uses($class));
$class = get_parent_class($class);
} while ($class);
return $features;
}
/**
* Require that the API supports a feature.
*
* If an Array is provided, this is treated as a require any of the features.
*
* @param string[]|string $feature The feature to test
* @throws \moodle_exception
*/
public function require_feature(array|string $feature): void {
if (!$this->implements_feature($feature)) {
if (is_array($feature)) {
$features = implode(', ', $feature);
throw new \moodle_exception(
"None of the possible feature are implemented in this Matrix Client: '{$features}'"
);
}
throw new \moodle_exception("The requested feature is not implemented in this Matrix Client: '{$feature}'");
}
}
/**
* Require that the API supports a list of features.
*
* All features specified will be required.
*
* If an array is provided as one of the features, any of the items in the nested array will be required.
*
* @param string[]|array[] $features The list of features required
*
* Here is an example usage:
* <code>
* $matrixapi->require_features([
*
* \communication_matrix\local\spec\features\create_room::class,
* [
* \communication_matrix\local\spec\features\get_room_info_v1::class,
* \communication_matrix\local\spec\features\get_room_info_v2::class,
* ]
* ])
* </code>
*/
public function require_features(array $features): void {
array_walk($features, [$this, 'require_feature']);
}
/**
* Get the URL of the server.
*
* @return string
*/
public function get_server_url(): string {
return $this->serverurl;
}
/**
* Query the supported versions, and any unstable features, from the server.
*
* Servers must implement the client versions API described here:
* - https://spec.matrix.org/latest/client-server-api/#get_matrixclientversions
*
* @param string $serverurl The server base
* @return null|\stdClass The list of supported versions and a list of enabled unstable features
*/
protected static function query_server_supports(string $serverurl): ?\stdClass {
// Attempt to return from the cache first.
$cache = \cache::make('communication_matrix', 'serverversions');
$serverkey = sha1($serverurl);
if ($cache->get($serverkey)) {
return $cache->get($serverkey);
}
// Not in the cache - fetch and store in the cache.
try {
$client = static::get_http_client();
$response = $client->get("{$serverurl}/_matrix/client/versions");
$supportsdata = json_decode(
json: $response->getBody(),
associative: false,
flags: JSON_THROW_ON_ERROR,
);
$cache->set($serverkey, $supportsdata);
return $supportsdata;
} catch (\GuzzleHttp\Exception\TransferException $e) {
return null;
}
}
/**
* Get the list of supported versions based on the available classes.
*
* @return array
*/
public static function get_supported_versions(): array {
$versions = [];
$iterator = new DirectoryIterator(__DIR__ . '/local/spec');
foreach ($iterator as $fileinfo) {
if ($fileinfo->isDir()) {
continue;
}
// Get the classname from the filename.
$classname = substr($fileinfo->getFilename(), 0, -4);
if (!preg_match('/^v\d+p\d+$/', $classname)) {
// @codeCoverageIgnoreStart
// This file does not fit the format v[MAJOR]p[MINOR]].
continue;
// @codeCoverageIgnoreEnd
}
$versions[$classname] = "v" . self::get_version_from_classname($classname);
}
return $versions;
}
/**
* Get the current token in use.
*
* @return string
*/
public function get_token(): string {
return $this->accesstoken;
}
/**
* Helper to fetch the HTTP Client for the instance.
*
* @return \core\http_client
*/
protected function get_client(): \core\http_client {
return static::get_http_client();
}
/**
* Helper to fetch the HTTP Client.
*
* @return \core\http_client
*/
protected static function get_http_client(): \core\http_client {
if (static::$client !== null) {
return static::$client;
}
// @codeCoverageIgnoreStart
return new http_client();
// @codeCoverageIgnoreEnd
}
/**
* Execute the specified command.
*
* @param command $command
* @return Response
*/
protected function execute(
command $command,
): Response {
$client = $this->get_client();
return $client->send(
$command,
$command->get_options(),
);
}
/**
* Get the API version of the current instance.
*
* @return string
*/
public function get_version(): string {
$reflect = new \ReflectionClass(static::class);
$classname = $reflect->getShortName();
return self::get_version_from_classname($classname);
}
/**
* Normalise an API version from a classname.
*
* @param string $classname The short classname, omitting any namespace or file extension
* @return string The normalised version
*/
protected static function get_version_from_classname(string $classname): string {
$classname = str_replace('v', '', $classname);
$classname = str_replace('p', '.', $classname);
return $classname;
}
/**
* Check if the API version is at least the specified version.
*
* @param string $minversion The minimum API version required
* @return bool
*/
public function meets_version(string $minversion): bool {
$thisversion = $this->get_version();
return version_compare($thisversion, $minversion) >= 0;
}
/**
* Assert that the API version is at least the specified version.
*
* @param string $minversion The minimum API version required
* @throws Exception
*/
public function requires_version(string $minversion): void {
if ($this->meets_version($minversion)) {
return;
}
throw new \moodle_exception("Matrix API version {$minversion} or higher is required for this command.");
}
}
@@ -0,0 +1,51 @@
<?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 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;
}
@@ -0,0 +1,146 @@
<?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;
use stdClass;
/**
* Class to manage the updates to the room information in db.
*
* @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_room {
private const TABLE = 'matrix_room';
/** @var \stdClass|null $record The matrix room record from db */
/**
* Load the matrix room record for the supplied processor.
* @param int $processorid
* @return null|self
*/
public static function load_by_processor_id(
int $processorid,
): ?self {
global $DB;
$record = $DB->get_record(self::TABLE, ['commid' => $processorid]);
if (!$record) {
return null;
}
return new self($record);
}
/**
* Matrix rooms constructor to load the matrix room information from matrix_room table.
*
* @param stdClass $record
*/
private function __construct(
private stdClass $record,
) {
}
/**
* Create matrix room data.
*
* @param int $processorid The id of the communication record
* @param string|null $topic The topic of the room for matrix
* @param string|null $roomid The id of the room from matrix
* @return self
*/
public static function create_room_record(
int $processorid,
?string $topic,
?string $roomid = null,
): self {
global $DB;
$roomrecord = (object) [
'commid' => $processorid,
'roomid' => $roomid,
'topic' => $topic,
];
$roomrecord->id = $DB->insert_record(self::TABLE, $roomrecord);
return self::load_by_processor_id($processorid);
}
/**
* Update matrix room data.
*
* @param string|null $roomid The id of the room from matrix
* @param string|null $topic The topic of the room for matrix
*/
public function update_room_record(
?string $roomid = null,
?string $topic = null,
): void {
global $DB;
if ($roomid !== null) {
$this->record->roomid = $roomid;
}
if ($topic !== null) {
$this->record->topic = $topic;
}
$DB->update_record(self::TABLE, $this->record);
}
/**
* Delete matrix room data.
*/
public function delete_room_record(): void {
global $DB;
$DB->delete_records(self::TABLE, ['commid' => $this->record->commid]);
unset($this->record);
}
/**
* Get the processor id.
*
* @return int
*/
public function get_processor_id(): int {
return $this->record->commid;
}
/**
* Get the matrix room id.
*
* @return string|null
*/
public function get_room_id(): ?string {
return $this->record->roomid;
}
/**
* Get the matrix room topic.
*
* @return string
*/
public function get_topic(): string {
return $this->record->topic ?? '';
}
}
@@ -0,0 +1,203 @@
<?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_user_manager to handle specific actions.
*
* @package communication_matrix
* @copyright 2023 Stevani Andolo <stevani.andolo@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class matrix_user_manager {
/**
* Prefix for Matrix usernames when they are detected as numeric.
*/
const MATRIX_USER_PREFIX = 'user';
/**
* Gets matrix user id from moodle.
*
* @param int $userid Moodle user id
* @return string|null
*/
public static function get_matrixid_from_moodle(
int $userid,
): ?string {
self::load_requirements();
$field = profile_user_record($userid);
$matrixprofilefield = get_config('communication_matrix', 'matrixuserid_field');
if ($matrixprofilefield === false) {
return null;
}
return $field->{$matrixprofilefield} ?? null;
}
/**
* Get a qualified matrix user id based on a Moodle username.
*
* @param string $username The moodle username to turn into a Matrix username
* @return string
*/
public static function get_formatted_matrix_userid(
string $username,
): string {
$username = preg_replace('/[@#$%^&*()+{}|<>?!,]/i', '.', $username);
$username = ltrim(rtrim($username, '.'), '.');
// Matrix/Synapse servers will not allow numeric usernames.
if (is_numeric($username)) {
$username = self::MATRIX_USER_PREFIX . $username;
}
$homeserver = self::get_formatted_matrix_home_server();
return "@{$username}:{$homeserver}";
}
/**
* Add user's Matrix user id.
*
* @param int $userid Moodle user id
* @param string $matrixuserid Matrix user id
*/
public static function set_matrix_userid_in_moodle(
int $userid,
string $matrixuserid,
): void {
$matrixprofilefield = self::get_profile_field_name();
$field = profile_get_custom_field_data_by_shortname($matrixprofilefield);
if ($field === null) {
return;
}
$userinfodata = (object) [
'id' => $userid,
'data' => $matrixuserid,
'fieldid' => $field->id,
"profile_field_{$matrixprofilefield}" => $matrixuserid,
];
profile_save_data($userinfodata);
}
/**
* Sets home server for user matrix id
*
* @return string
*/
public static function get_formatted_matrix_home_server(): string {
$homeserver = get_config('communication_matrix', 'matrixhomeserverurl');
if ($homeserver === false) {
throw new \moodle_exception('Unknown matrix homeserver url');
}
$homeserver = parse_url($homeserver)['host'];
if (str_starts_with($homeserver, 'www.')) {
$homeserver = str_replace('www.', '', $homeserver);
}
return $homeserver;
}
/**
* Insert "Communication" category and "matrixuserid" field.
*
* @return string
*/
public static function create_matrix_user_profile_fields(): string {
global $CFG, $DB;
require_once($CFG->dirroot . '/user/profile/definelib.php');
require_once($CFG->dirroot . '/user/profile/field/text/define.class.php');
// Check if communication category exists.
$categoryname = get_string('communication', 'core_communication');
$category = $DB->count_records('user_info_category', ['name' => $categoryname]);
if ($category < 1) {
$data = new \stdClass();
$data->sortorder = $DB->count_records('user_info_category') + 1;
$data->name = $categoryname;
$data->id = $DB->insert_record('user_info_category', $data);
$createdcategory = $DB->get_record('user_info_category', ['id' => $data->id]);
$categoryid = $createdcategory->id;
\core\event\user_info_category_created::create_from_category($createdcategory)->trigger();
} else {
$category = $DB->get_record('user_info_category', ['name' => $categoryname]);
$categoryid = $category->id;
}
set_config('communication_category_field', $categoryname, 'core_communication');
// Check if matrixuserid exists in user_info_field table.
$matrixuserid = $DB->count_records('user_info_field', [
'shortname' => 'matrixuserid',
'categoryid' => $categoryid,
]);
if ($matrixuserid < 1) {
$profileclass = new \profile_define_text();
$data = (object) [
'shortname' => 'matrixuserid',
'name' => get_string('matrixuserid', 'communication_matrix'),
'datatype' => 'text',
'categoryid' => $categoryid,
'forceunique' => 1,
'visible' => 0,
'locked' => 1,
'param1' => 30,
'param2' => 2048,
];
$profileclass->define_save($data);
set_config('matrixuserid_field', 'matrixuserid', 'communication_matrix');
return 'matrixuserid';
}
}
/**
* Get the profile field name, creating the profiel field if it does not exist.
*
* @return string
*/
protected static function get_profile_field_name(): string {
self::load_requirements();
$matrixprofilefield = get_config('communication_matrix', 'matrixuserid_field');
if ($matrixprofilefield === false) {
$matrixprofilefield = self::create_matrix_user_profile_fields();
}
return $matrixprofilefield;
}
/**
* Load requirements for profile field management.
*
* This is just a helper to keep loading legacy files isolated.
*/
protected static function load_requirements(): void {
global $CFG;
require_once("{$CFG->dirroot}/user/profile/lib.php");
}
}
@@ -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/>.
namespace communication_matrix\privacy;
use core_privacy\local\metadata\null_provider;
/**
* Privacy Subsystem for communication_matrix implementing null_provider.
*
* @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
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -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,
],
],
];
@@ -0,0 +1,42 @@
<?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/>.
/**
* Cache definition for the Matrix Communication plugin.
*
* @package communication_matrix
* @category cache
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$definitions = [
// Used to store processed lang files.
// The keys used are the revision, lang and component of the string file.
// The static acceleration size has been based upon student access of the site.
'serverversions' => [
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'simpledata' => true,
'staticacceleration' => true,
'staticaccelerationsize' => 1,
'canuselocalstore' => true,
// Cache for one day.
'ttl' => 60 * 60 * 24,
],
];
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="communication/provider/matrix/db" VERSION="20230719" COMMENT="Stores the matrix room information associated with the communication instance."
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="matrix_room" COMMENT="Stores the matrix room information associated with the communication instance.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="commid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of the communication record"/>
<FIELD NAME="roomid" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="ID of the matrix room instance"/>
<FIELD NAME="topic" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false" COMMENT="Topic of the matrix room instance."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_commid" TYPE="foreign" FIELDS="commid" REFTABLE="communication" REFFIELDS="id" COMMENT="Foreign key for communication reference"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
@@ -0,0 +1,61 @@
<?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/>.
/**
* Install steps for communication_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
*/
/**
* Upgrade procedures for the matrix plugin.
*
* @return bool
*/
function xmldb_communication_matrix_upgrade($oldversion) {
global $DB;
$dbman = $DB->get_manager();
if ($oldversion < 2023060101) {
$table = new xmldb_table('matrix_rooms');
$field = new xmldb_field('topic', XMLDB_TYPE_CHAR, '255', null, false, false, null, 'roomid');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Plugin savepoint reached.
upgrade_plugin_savepoint(true, 2023060101, 'communication', 'matrix');
}
if ($oldversion < 2023071900) {
$table = new xmldb_table('matrix_rooms');
$dbman->rename_table($table, 'matrix_room');
// Plugin savepoint reached.
upgrade_plugin_savepoint(true, 2023071900, 'communication', 'matrix');
}
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,40 @@
<?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/>.
/**
* Strings for component communication_matrix, language 'en'.
*
* @package communication_matrix
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['cachedef_serverversions'] = 'Matrix server version information for running servers';
$string['matrixuserid'] = 'Matrix user ID';
$string['matrixhomeserverurl'] = 'Homeserver URL';
$string['matrixhomeserverurl_desc'] = 'The URL of the Synapse homeserver to connect to, for user and room creation.';
$string['matrixaccesstoken'] = 'Access token';
$string['matrixaccesstoken_desc'] = 'Access token for the account which will perform actions on the homeserver.';
$string['matrixelementurl'] = 'Element web URL';
$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'] = 'The Matrix communication plugin does not store any personal data.';
// Deprecated since Moodle 4.4.
$string['matrixrefreshtoken'] = 'Refresh token';
$string['matrixrefreshtoken_desc'] = 'Admin refresh token to be associated with the access token.';
@@ -0,0 +1,2 @@
matrixrefreshtoken,communication_matrix
matrixrefreshtoken_desc,communication_matrix
@@ -0,0 +1,41 @@
<?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/>.
/**
* Matrix communication plugin settings.
*
* @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;
if ($hassiteconfig) {
// Home server URL.
$name = new lang_string('matrixhomeserverurl', 'communication_matrix');
$desc = new lang_string('matrixhomeserverurl_desc', 'communication_matrix');
$settings->add(new admin_setting_configtext('communication_matrix/matrixhomeserverurl', $name, $desc, ''));
// Access token.
$name = new lang_string('matrixaccesstoken', 'communication_matrix');
$desc = new lang_string('matrixaccesstoken_desc', 'communication_matrix');
$settings->add(new admin_setting_configpasswordunmask('communication_matrix/matrixaccesstoken', $name, $desc, ''));
// Element web URL.
$name = new lang_string('matrixelementurl', 'communication_matrix');
$settings->add(new admin_setting_configtext('communication_matrix/matrixelementurl', $name, '', ''));
}
@@ -0,0 +1,66 @@
<?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/>.
// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use communication_matrix\matrix_test_helper_trait;
use Moodle\BehatExtension\Exception\SkippedException;
require_once(__DIR__ . '/../matrix_test_helper_trait.php');
require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
require_once(__DIR__ . '/../../../../tests/communication_test_helper_trait.php');
/**
* Class behat_communication_matrix for behat custom steps and configuration for matrix.
*
* @package communication_matrix
* @category test
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @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;
/**
* BeforeScenario hook to reset the mock server.
*
* @BeforeScenario @communication_matrix
*
* @param BeforeScenarioScope $scope
*/
public function before_scenario(BeforeScenarioScope $scope) {
if (defined('TEST_COMMUNICATION_MATRIX_MOCK_SERVER')) {
$this->reset_mock();
}
}
/**
* Setup and configure and mock server for matrix.
*
* @Given /^a Matrix mock server is configured$/
*/
public function initialize_mock_server(): void {
if (!defined('TEST_COMMUNICATION_MATRIX_MOCK_SERVER')) {
throw new SkippedException(
'The TEST_COMMUNICATION_MATRIX_MOCK_SERVER constant must be defined to run communication_matrix tests'
);
}
$this->setup_communication_configs();
$this->initialise_mock_configs();
}
}
@@ -0,0 +1,34 @@
@communication @communication_matrix
Feature: Communication matrix form field
In order to create a new communication room in matrix
As a teacher
I can update the room the information from course
Background: Make sure the mock server is initialized and a course is created for teacher
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Test course | Test course | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | Test course | editingteacher |
@javascript
Scenario: I can add room name and topic for matrix room
Given a Matrix mock server is configured
And I am on the "Test course" "Course" page logged in as "teacher1"
When I navigate to "Communication" in current page administration
And I set the following fields to these values:
| selectedcommunication | communication_matrix |
And I wait to be redirected
And I set the following fields to these values:
| communication_matrixroomname | Sampleroomname |
| matrixroomtopic | Sampleroomtopic |
And I should see "Room name"
And I should see "Room topic"
And I press "Save changes"
And I navigate to "Communication" in current page administration
Then the field "Room name" matches value "Sampleroomname"
And the field "Room topic" matches value "Sampleroomtopic"
@@ -0,0 +1,52 @@
@communication @communication_matrix
Feature: Display communication room status banner
Show a banner depending on the room status
As a teacher or admin
Background:
Given a Matrix mock server is configured
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category | selectedcommunication | communicationroomname |
| Test course | Test course | 0 | communication_matrix | matrixroom |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | Test course | editingteacher |
| student1 | Test course | student |
Scenario: I can see the room has been created and in a pending status
When I am on the "Test course" "Course" page logged in as "teacher1"
Then I should see "Your Matrix room will be ready soon." in the "page-content" "region"
When I am on the "Test course" "Course" page logged in as "student1"
# Not for students to see.
Then I should not see "Your Matrix room will be ready soon." in the "page-content" "region"
Scenario: I can see the room has been created and ready to access
When I run all adhoc tasks
And I am on the "Test course" "Course" page logged in as "teacher1"
Then I should see "Your Matrix room is ready." in the "page-content" "region"
# This is a one time message per user.
When I reload the page
Then I should not see "Your Matrix room is ready." in the "page-content" "region"
# Not for students to see.
When I am on the "Test course" "Course" page logged in as "student1"
Then I should not see "Your Matrix room is ready." in the "page-content" "region"
Scenario: Enabling or disabling the matrix plugin hides the banner accordingly
Given I am on the "Test course" "Course" page logged in as "teacher1"
Then I should see "Your Matrix room will be ready soon." in the "page-content" "region"
When I log in as "admin"
And I navigate to "Plugins > Communication > Manage communication providers" in site administration
And I should see "Matrix"
And I click on "Disable" "link" in the "Matrix" "table_row"
And I am on the "Test course" "Course" page logged in as "teacher1"
And I should not see "Your Matrix room will be ready soon." in the "page-content" "region"
And I log in as "admin"
And I navigate to "Plugins > Communication > Manage communication providers" in site administration
And I should see "Matrix"
And I click on "Enable" "link" in the "Matrix" "table_row"
And I am on the "Test course" "Course" page logged in as "teacher1"
Then I should see "Your Matrix room will be ready soon." in the "page-content" "region"
@@ -0,0 +1,571 @@
<?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;
use core\context;
use core_communication\api;
use core_communication\communication_test_helper_trait;
use core_communication\processor;
use stored_file;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/matrix_test_helper_trait.php');
require_once(__DIR__ . '/../../../tests/communication_test_helper_trait.php');
/**
* Class communication_feature_test to test the matrix features implemented using the core interfaces.
*
* @package communication_matrix
* @category test
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @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 {
use matrix_test_helper_trait;
use communication_test_helper_trait;
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setup_communication_configs();
$this->initialise_mock_server();
}
/**
* Test create or update chat room.
*
* @covers ::create_chat_room
*/
public function test_create_chat_room(): void {
// Set up the test data first.
$communication = \core_communication\api::load_by_instance(
context: \core\context\system::instance(),
component: 'communication_matrix',
instancetype: 'example',
instanceid: 1,
provider: 'communication_matrix',
);
$communication->create_and_configure_room(
communicationroomname: 'Room name',
instance: (object) [
'matrixroomtopic' => 'A fun topic',
],
);
// phpcs:ignore moodle.Commenting.InlineComment.DocBlock
/** @var communication_feature */
$provider = $communication->get_room_provider();
$this->assertInstanceOf(
communication_feature::class,
$provider,
);
// Run the create_chat_room task.
$result = $provider->create_chat_room();
$this->assertTrue($result);
// Ensure that a room_id was set.
$this->assertNotEmpty($provider->get_room_id());
// Fetch the back office room data.
$remoteroom = $this->backoffice_get_room();
// The roomid set in the database must match the one set on the remote server.
$this->assertEquals(
$remoteroom->room_id,
$provider->get_room_id(),
);
// The name is a feature of the communication API itself.
$this->assertEquals(
'Room name',
$communication->get_room_name(),
);
$this->assertEquals(
$communication->get_room_name(),
$remoteroom->name,
);
// The topic is a Matrix feature.
$roomconfig = $provider->get_room_configuration();
$this->assertEquals(
'A fun topic',
$roomconfig->get_topic(),
);
$this->assertEquals(
$remoteroom->topic,
$roomconfig->get_topic(),
);
// The avatar features are checked in a separate test.
}
/**
* Test update of a chat room.
*
* @covers ::update_chat_room
*/
public function test_update_chat_room(): void {
$communication = $this->create_room(
roomname: 'Our room name',
roomtopic: 'Our room topic',
);
// phpcs:ignore moodle.Commenting.InlineComment.DocBlock
/** @var communication_feature */
$provider = $communication->get_room_provider();
$this->assertInstanceOf(
communication_feature::class,
$provider,
);
// Update the room name.
// Note: We have to update the record via the API, and then call the provider update method.
// That's because the update is performed asynchronously.
$communication->update_room(
communicationroomname: 'Our updated room name',
);
$provider->reload();
// Now call the provider's update method.
$provider->update_chat_room();
// And assert that it was updated remotely.
$remoteroom = $this->backoffice_get_room();
$this->assertEquals(
'Our updated room name',
$communication->get_room_name(),
);
$this->assertEquals(
$communication->get_room_name(),
$remoteroom->name,
);
// The remote topic should not have changed.
$this->assertEquals(
'Our room topic',
$remoteroom->topic,
);
// Now update just the topic.
// First in the local API.
$communication->update_room(
instance: (object) [
'matrixroomtopic' => 'Our updated room topic',
],
);
$provider->reload();
// Then call the provider's update method to actually perform the change.
$provider->update_chat_room();
// And assert that it was updated remotely.
$remoteroom = $this->backoffice_get_room();
$this->assertEquals(
'Our updated room topic',
$provider->get_room_configuration()->get_topic(),
);
// The remote topic should have been updated.
$this->assertEquals(
'Our updated room topic',
$remoteroom->topic,
);
// The name should not have changed.
$this->assertEquals(
'Our updated room name',
$communication->get_room_name(),
);
}
/**
* Test delete chat room.
*
* @covers ::delete_chat_room
*/
public function test_delete_chat_room(): void {
$communication = $this->create_room();
$processor = $communication->get_processor();
$provider = $communication->get_room_provider();
$room = matrix_room::load_by_processor_id($processor->get_id());
// Run the delete method.
$this->assertTrue($provider->delete_chat_room());
// The record of the room should have been removed.
$this->assertNull(matrix_room::load_by_processor_id($processor->get_id()));
// But the room itself shoudl exist.
$matrixroomdata = $this->get_matrix_room_data($room->get_room_id());
$this->assertNotEmpty($matrixroomdata);
$this->assertEquals($processor->get_room_name(), $matrixroomdata->name);
$this->assertEquals($room->get_topic(), $matrixroomdata->topic);
}
/**
* Test update room avatar.
*
* @covers ::update_room_avatar
* @dataProvider avatar_provider
*/
public function test_update_room_avatar(
?string $before,
?string $after,
): void {
$this->setAdminUser();
// Create a new draft file.
$logo = $this->create_communication_file('moodle_logo.jpg', 'logo.jpg');
$circle = $this->create_communication_file('circle.png', 'circle.png');
if ($before === 'logo') {
$before = $logo;
} else if ($before === 'circle') {
$before = $circle;
}
if ($after === 'logo') {
$after = $logo;
} else if ($after === 'circle') {
$after = $circle;
}
$communication = $this->create_matrix_room(
component: 'communication_matrix',
itemtype: 'example_room',
itemid: 1,
roomname: 'Example room name',
roomavatar: $before,
);
// Confirm that the avatar was set remotely.
$remoteroom = $this->backoffice_get_room();
if ($before) {
$this->assertStringEndsWith($before->get_filename(), $remoteroom->avatar);
$avatarcontent = download_file_content($remoteroom->avatar);
$this->assertEquals($before->get_content(), $avatarcontent);
} else {
$this->assertEmpty($remoteroom->avatar);
}
// Reload the API instance as the information stored has changed.
$communication->reload();
// Update the avatar with the 'after' avatar.
$communication->update_room(
avatar: $after,
);
$this->run_all_adhoc_tasks();
// Confirm that the avatar was updated remotely.
$remoteroom = $this->backoffice_get_room();
if ($after) {
$this->assertStringEndsWith($after->get_filename(), $remoteroom->avatar);
$avatarcontent = download_file_content($remoteroom->avatar);
$this->assertEquals($after->get_content(), $avatarcontent);
} else {
$this->assertEmpty($remoteroom->avatar);
}
}
/**
* Tests for setting and updating the room avatar.
*
* @return array
*/
public static function avatar_provider(): array {
return [
'Empty to avatar' => [
null,
'circle',
],
'Avatar to empty' => [
'circle',
null,
],
'Avatar to new avatar' => [
'circle',
'logo',
],
];
}
/**
* Test get chat room url.
*
* @covers ::get_chat_room_url
*/
public function test_get_chat_room_url(): void {
$communication = $this->create_room();
$provider = $communication->get_room_provider();
$url = $provider->get_chat_room_url();
$this->assertNotNull($url);
// Fetch the room information from the server.
$remoteroom = $this->backoffice_get_room();
$this->assertStringEndsWith(
$remoteroom->room_id,
$url,
);
}
/**
* Test create members.
*
* @covers ::create_members
* @covers ::add_registered_matrix_user_to_room
*/
public function test_create_members(): void {
$user = $this->getDataGenerator()->create_user();
$communication = $this->create_room(
members: [
$user->id,
],
);
$remoteroom = $this->backoffice_get_room();
$this->assertCount(1, $remoteroom->members);
$member = reset($remoteroom->members);
$this->assertStringStartsWith("@{$user->username}", $member->userid);
}
/**
* Test add/remove members from room.
*
* @covers ::remove_members_from_room
* @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();
$user2 = $this->getDataGenerator()->create_user();
$communication = $this->create_room();
$provider = $communication->get_room_user_provider();
$remoteroom = $this->backoffice_get_room();
$this->assertCount(0, $remoteroom->members);
// Add the members to the room.
$provider->add_members_to_room([$user->id, $user2->id]);
// Ensure that they have been created.
$remoteroom = $this->backoffice_get_room();
$this->assertCount(2, $remoteroom->members);
$userids = array_map(fn($member) => $member->userid, $remoteroom->members);
$userids = array_map(fn($userid) => substr($userid, 0, strpos($userid, ':')), $userids);
$this->assertContains("@{$user->username}", $userids);
$this->assertContains("@{$user2->username}", $userids);
// Remove member from matrix room.
$provider->remove_members_from_room([$user->id]);
// Ensure that they have been removed.
$remoteroom = $this->backoffice_get_room();
$members = (array) $remoteroom->members;
$this->assertCount(1, $members);
$userids = array_map(fn ($member) => $member->userid, $members);
$userids = array_map(fn ($userid) => substr($userid, 0, strpos($userid, ':')), $userids);
$this->assertNotContains("@{$user->username}", $userids);
$this->assertContains("@{$user2->username}", $userids);
}
/**
* Test 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');
$coursecontext = \context_course::instance($course->id);
$user = $this->get_user();
$communication = $this->create_room(
component: 'core_course',
itemtype: 'coursecommunication',
itemid: $course->id,
roomname: 'sampleroom',
roomtopic: 'sampltopic',
roomavatar: null,
members: [$user->id],
context: $coursecontext,
);
$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.
$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(
context: $coursecontext,
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(
context: \core\context\course::instance($course->id),
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.
*
* @param null|string $component
* @param null|string $itemtype
* @param null|int $itemid
* @param null|string $roomname
* @param null|string $roomtopic
* @param null|stored_file $roomavatar
* @param array $members
* @return api
*/
protected function create_room(
?string $component = 'communication_matrix',
?string $itemtype = 'example',
?int $itemid = 1,
?string $roomname = null,
?string $roomtopic = null,
?\stored_file $roomavatar = null,
array $members = [],
?context $context = null,
): \core_communication\api {
// Create a new room.
$communication = \core_communication\api::load_by_instance(
context: $context ?? \core\context\system::instance(),
component: $component,
instancetype: $itemtype,
instanceid: $itemid,
provider: 'communication_matrix',
);
$communication->create_and_configure_room(
communicationroomname: $roomname ?? 'Room name',
avatar: $roomavatar,
instance: (object) [
'matrixroomtopic' => $roomtopic ?? 'A fun topic',
],
);
$communication->add_members_to_room($members);
// Run the adhoc task.
$this->run_all_adhoc_tasks();
$communication->reload();
return $communication;
}
/**
* Test if the selected provider is configured.
*
* @covers ::is_configured
*/
public function test_is_configured(): void {
$course = $this->get_course();
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id
);
$this->assertTrue($communicationprocessor->get_room_provider()->is_configured());
// Unset communication_matrix settings.
unset_config('matrixhomeserverurl', 'communication_matrix');
unset_config('matrixaccesstoken', 'communication_matrix');
$this->assertFalse($communicationprocessor->get_room_provider()->is_configured());
}
}
@@ -0,0 +1,52 @@
<?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\tests\fixtures;
use core\http_client;
/**
* Tests for the api_base class.
*
* @package communication_matrix
* @category test
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mocked_matrix_client extends \communication_matrix\matrix_client {
/**
* Public variant of the constructor.
*/
public function __construct() {
parent::__construct(...func_get_args());
}
/**
* Reset the test client.
*/
public static function reset_client(): void {
self::$client = null;
}
/**
* Set the http_client to the client specified.
*
* @param http_client $client
*/
public static function set_client(http_client $client): void {
self::$client = $client;
}
}
@@ -0,0 +1,411 @@
<?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;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use ReflectionMethod;
defined('MOODLE_INTERNAL') || die();
require_once(dirname(__DIR__) . '/matrix_client_test_trait.php');
/**
* Tests for the Matrix command class.
*
* @package communication_matrix
* @category test
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \communication_matrix\local\command
* @coversDefaultClass \communication_matrix\local\command
*/
class command_test extends \advanced_testcase {
use \communication_matrix\matrix_client_test_trait;
/**
* Test instantiation of a command when no method is provided.
*/
public function test_standard_instantiation(): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: 'example/endpoint',
);
// Check the standard functionality.
$this->assertEquals('/example/endpoint', $command->getUri()->getPath());
$this->assertEquals('PUT', $command->getMethod());
$this->assertArrayHasKey('Authorization', $command->getHeaders());
}
/**
* Test instantiation of a command when no method is provided.
*/
public function test_instantiation_without_auth(): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: 'example/endpoint',
requireauthorization: false,
);
// Check the standard functionality.
$this->assertEquals('/example/endpoint', $command->getUri()->getPath());
$this->assertEquals('PUT', $command->getMethod());
$this->assertArrayNotHasKey('Authorization', $command->getHeaders());
}
/**
* Test processing of command URL properties.
*
* @dataProvider url_parsing_provider
* @param string $url
* @param array $params
* @param string $expected
*/
public function test_url_parsing(
string $url,
array $params,
string $expected,
): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: $url,
params: $params,
);
$this->assertEquals($expected, $command->getUri()->getPath());
}
/**
* Data provider for url parsing tests.
*
* @return array
*/
public static function url_parsing_provider(): array {
return [
[
'example/:id/endpoint',
[':id' => '39492'],
'/example/39492/endpoint',
],
[
'example/:id/endpoint/:id',
[':id' => '39492'],
'/example/39492/endpoint/39492',
],
[
'example/:id/endpoint/:id/:name',
[
':id' => '39492',
':name' => 'matrix',
],
'/example/39492/endpoint/39492/matrix',
],
];
}
/**
* Test processing of command URL properties with an array which contains untranslated parameters.
*/
public function test_url_parsing_extra_properties(): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$this->expectException(\OutOfRangeException::class);
$this->expectExceptionMessage("URL contains untranslated parameters 'example/:id/endpoint'");
new command(
$instance,
method: 'PUT',
endpoint: 'example/:id/endpoint',
);
}
/**
* Test processing of command URL properties with an array which contains untranslated parameters.
*/
public function test_url_parsing_unused_properites(): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$this->expectException(\OutOfRangeException::class);
$this->expectExceptionMessage("Parameter not found in URL ':id'");
new command(
$instance,
method: 'PUT',
endpoint: 'example/:ids/endpoint',
params: [
':id' => 12345,
],
);
}
/**
* Test the parameter fetching, processing, and parsing.
*
* @dataProvider parameter_and_option_provider
* @param string $endpoint
* @param array $params
* @param array $remainingparams
* @param array $allparams
* @param array $options
*/
public function test_parameters(
string $endpoint,
array $params,
array $remainingparams,
array $allparams,
array $options,
): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: $endpoint,
params: $params,
);
$this->assertSame($remainingparams, $command->get_remaining_params());
$this->assertSame($allparams, $command->get_all_params());
$this->assertSame($options, $command->get_options());
}
/**
* Data provider for parameter tests.
*
* @return array
*/
public static function parameter_and_option_provider(): array {
$command = [
'method' => 'PUT',
'endpoint' => 'example/:id/endpoint',
];
return [
'no parameters' => [
'endpoint' => 'example/endpoint',
'params' => [],
'remainingparams' => [],
'allparams' => [],
'options' => [
'json' => [],
],
],
'named params' => [
'endpoint' => 'example/:id/endpoint',
'params' => [
':id' => 12345,
],
'remainingparams' => [],
'allparams' => [
':id' => 12345,
],
'options' => [
'json' => [],
],
],
'mixture of params' => [
'endpoint' => 'example/:id/endpoint',
'params' => [
':id' => 12345,
'name' => 'matrix',
],
'remainingparams' => [
'name' => 'matrix',
],
'allparams' => [
':id' => 12345,
'name' => 'matrix',
],
'options' => [
'json' => [
'name' => 'matrix',
],
],
],
];
}
/**
* Test the query parameter handling.
*
* @dataProvider query_provider
* @param array $query
* @param string $expected
*/
public function test_query_parameters(
array $query,
string $expected,
): void {
// The query parameter is only added at the time we call send.
// That's because it can only be provided to Guzzle as an Option, not as part of the URL.
// Options can only be applied at time of transfer.
// Unfortuantely that leads to slightly less ideal testing that we'd like here.
$mock = new MockHandler();
$instance = $this->get_mocked_instance_for_version(
'v1.7',
mock: $mock,
);
$mock->append(function (Request $request) use ($expected): Response {
$this->assertSame(
$expected,
$request->getUri()->getQuery(),
);
return new Response();
});
$command = new command(
$instance,
method: 'PUT',
endpoint: 'example/endpoint',
query: $query,
);
$execute = new ReflectionMethod($instance, 'execute');
$execute->invoke($instance, $command);
}
/**
* Data provider for query parameter tests.
* @return array
*/
public static function query_provider(): array {
return [
'no query' => [
'query' => [],
'expected' => '',
],
'single query' => [
'query' => [
'name' => 'matrix',
],
'expected' => 'name=matrix',
],
'multiple queries' => [
'query' => [
'name' => 'matrix',
'type' => 'room',
],
'expected' => 'name=matrix&type=room',
],
];
}
/**
* Test the sendasjson constructor parameter.
*
* @dataProvider sendasjson_provider
* @param bool $sendasjson
* @param string $endpoint
* @param array $params
* @param array $remainingparams
* @param array $allparams
* @param array $expectedoptions
*/
public function test_send_as_json(
bool $sendasjson,
string $endpoint,
array $params,
array $remainingparams,
array $allparams,
array $expectedoptions,
): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: $endpoint,
params: $params,
sendasjson: $sendasjson,
);
$this->assertSame($remainingparams, $command->get_remaining_params());
$this->assertSame($allparams, $command->get_all_params());
$this->assertSame($expectedoptions, $command->get_options());
}
/**
* Test the sendasjosn option to the command constructor.
*
* @return array
*/
public static function sendasjson_provider(): array {
return [
'As JSON' => [
'sendasjon' => true,
'endpoint' => 'example/:id/endpoint',
'params' => [
':id' => 12345,
'name' => 'matrix',
],
'remainingparams' => [
'name' => 'matrix',
],
'allparams' => [
':id' => 12345,
'name' => 'matrix',
],
'expectedoptions' => [
'json' => [
'name' => 'matrix',
],
],
],
'Not as JSON' => [
'sendasjson' => false,
'endpoint' => 'example/:id/endpoint',
'params' => [
':id' => 12345,
'name' => 'matrix',
],
'remainingparams' => [
'name' => 'matrix',
],
'allparams' => [
':id' => 12345,
'name' => 'matrix',
],
'expectedoptions' => [
],
],
];
}
/**
* Test the sendasjosn option to the command constructor.
*/
public function test_ignorehttperrors(): void {
$instance = $this->get_mocked_instance_for_version('v1.7');
$command = new command(
$instance,
method: 'PUT',
endpoint: 'example/endpoint',
ignorehttperrors: true,
);
$options = $command->get_options();
$this->assertArrayHasKey('http_errors', $options);
$this->assertFalse($options['http_errors']);
}
}
@@ -0,0 +1,458 @@
<?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;
use communication_matrix\local\command;
use communication_matrix\local\spec\v1p7;
use communication_matrix\local\spec\features;
use communication_matrix\tests\fixtures\mocked_matrix_client;
use core\http_client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/matrix_client_test_trait.php');
/**
* Tests for the matrix_client class.
*
* @package communication_matrix
* @category test
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \communication_matrix\matrix_client
* @coversDefaultClass \communication_matrix\matrix_client
*/
class matrix_client_test extends \advanced_testcase {
use matrix_client_test_trait;
/**
* Data provider for valid calls to ::instance.
* @return array
*/
public static function instance_provider(): array {
$testcases = [
'Standard versions' => [
null,
v1p7::class,
],
];
// Remove a couple of versions.
$versions = self::get_current_versions();
array_pop($versions);
array_pop($versions);
$testcases['Older server'] = [
$versions,
array_key_last($versions),
];
// Limited version compatibility, including newer than we support now.
$testcases['Newer versions with crossover'] = [
[
'v1.6',
'v1.7',
'v7.9',
],
\communication_matrix\local\spec\v1p7::class,
];
return $testcases;
}
/**
* Test that the instance method returns a valid instance for the given versions.
*
* @dataProvider instance_provider
* @param array|null $versions
* @param string $expectedversion
*/
public function test_instance(
?array $versions,
string $expectedversion,
): void {
// Create a mock and queue two responses.
$mock = new MockHandler([
$this->get_mocked_version_response($versions),
]);
$handlerstack = HandlerStack::create($mock);
$container = [];
$history = Middleware::history($container);
$handlerstack->push($history);
$client = new http_client(['handler' => $handlerstack]);
mocked_matrix_client::set_client($client);
$instance = mocked_matrix_client::instance(
'https://example.com',
'testtoken',
);
$this->assertInstanceOf(matrix_client::class, $instance);
// Only the version API has been called.
$this->assertCount(1, $container);
$request = reset($container);
$this->assertEquals('/_matrix/client/versions', $request['request']->getUri()->getPath());
// The client should be a v1p7 client as that is the highest compatible version.
$this->assertInstanceOf($expectedversion, $instance);
}
/**
* Test that the instance method returns a valid instance for the given versions.
*/
public function test_instance_cached(): void {
$mock = new MockHandler([
$this->get_mocked_version_response(),
$this->get_mocked_version_response(),
]);
$handlerstack = HandlerStack::create($mock);
$container = [];
$history = Middleware::history($container);
$handlerstack->push($history);
$client = new http_client(['handler' => $handlerstack]);
mocked_matrix_client::set_client($client);
$instance = mocked_matrix_client::instance('https://example.com', 'testtoken');
$this->assertInstanceOf(matrix_client::class, $instance);
// Only the version API has been called.
$this->assertCount(1, $container);
// Call the API again. It should not lead to additional fetches.
$instance = mocked_matrix_client::instance('https://example.com', 'testtoken');
$instance = mocked_matrix_client::instance('https://example.com', 'testtoken');
$this->assertCount(1, $container);
// But a different endpoint will.
$instance = mocked_matrix_client::instance('https://example.org', 'testtoken');
$this->assertCount(2, $container);
}
/**
* Test that the instance method throws an appropriate exception if no support is found.
*/
public function test_instance_no_support(): void {
// Create a mock and queue two responses.
$mock = new MockHandler([
$this->get_mocked_version_response(['v99.9']),
]);
$handlerstack = HandlerStack::create($mock);
$container = [];
$history = Middleware::history($container);
$handlerstack->push($history);
$client = new http_client(['handler' => $handlerstack]);
mocked_matrix_client::set_client($client);
$this->expectException(moodle_exception::class);
$this->expectExceptionMessage('No supported Matrix API versions found.');
mocked_matrix_client::instance(
'https://example.com',
'testtoken',
);
}
/**
* Test the feature implementation check methods.
*
* @covers ::implements_feature
* @covers ::get_supported_versions
* @dataProvider implements_feature_provider
* @param string $version
* @param array|string $features
* @param bool $expected
*/
public function test_implements_feature(
string $version,
array|string $features,
bool $expected,
): void {
$instance = $this->get_mocked_instance_for_version($version);
$this->assertEquals($expected, $instance->implements_feature($features));
}
/**
* Test the feature implementation requirement methods.
*
* @covers ::implements_feature
* @covers ::get_supported_versions
* @covers ::require_feature
* @dataProvider implements_feature_provider
* @param string $version
* @param array|string $features
* @param bool $expected
*/
public function test_require_feature(
string $version,
array|string $features,
bool $expected,
): void {
$instance = $this->get_mocked_instance_for_version($version);
if ($expected) {
$this->assertEmpty($instance->require_feature($features));
} else {
$this->expectException('moodle_exception');
$instance->require_feature($features);
}
}
/**
* Test the feature implementation requirement methods for a require all.
*
* @covers ::implements_feature
* @covers ::get_supported_versions
* @covers ::require_feature
* @covers ::require_features
* @dataProvider require_features_provider
* @param string $version
* @param array|string $features
* @param bool $expected
*/
public function test_require_features(
string $version,
array|string $features,
bool $expected,
): void {
$instance = $this->get_mocked_instance_for_version($version);
if ($expected) {
$this->assertEmpty($instance->require_features($features));
} else {
$this->expectException('moodle_exception');
$instance->require_features($features);
}
}
/**
* Data provider for feature implementation check tests.
*
* @return array
*/
public static function implements_feature_provider(): array {
return [
'Basic supported feature' => [
'v1.7',
features\matrix\media_create_v1::class,
true,
],
'Basic unsupported feature' => [
'v1.6',
features\matrix\media_create_v1::class,
false,
],
'[supported] as array' => [
'v1.6',
[features\matrix\create_room_v3::class],
true,
],
'[supported, supported] as array' => [
'v1.6',
[
features\matrix\create_room_v3::class,
features\matrix\update_room_avatar_v3::class,
],
true,
],
'[unsupported] as array' => [
'v1.6',
[
features\matrix\media_create_v1::class,
],
false,
],
'[unsupported, supported] as array' => [
'v1.6',
[
features\matrix\media_create_v1::class,
features\matrix\update_room_avatar_v3::class,
],
true,
],
];
}
/**
* Data provider for feature implementation check tests.
*
* @return array
*/
public static function require_features_provider(): array {
// We'll just add to the standard testcases.
$testcases = array_map(static function (array $testcase): array {
$testcase[1] = [$testcase[1]];
return $testcase;
}, self::implements_feature_provider());
$testcases['Require many supported features'] = [
'v1.6',
[
features\matrix\create_room_v3::class,
features\matrix\update_room_avatar_v3::class,
],
true,
];
$testcases['Require many including an unsupported feature'] = [
'v1.6',
[
features\matrix\create_room_v3::class,
features\matrix\media_create_v1::class,
],
false,
];
$testcases['Require many including an unsupported feature which has an alternate'] = [
'v1.6',
[
features\matrix\create_room_v3::class,
[
features\matrix\media_create_v1::class,
features\matrix\update_room_avatar_v3::class,
],
],
true,
];
return $testcases;
}
/**
* Test the get_version method.
*
* @param string $version
* @param string $expectedversion
* @dataProvider get_version_provider
* @covers ::get_version
* @covers ::get_version_from_classname
*/
public function test_get_version(
string $version,
string $expectedversion,
): void {
$instance = $this->get_mocked_instance_for_version($version);
$this->assertEquals($expectedversion, $instance->get_version());
}
/**
* Data provider for get_version tests.
*
* @return array
*/
public static function get_version_provider(): array {
return [
['v1.1', '1.1'],
['v1.7', '1.7'],
];
}
/**
* Tests the meets_version method.
*
* @param string $version The version of the API to test against
* @param string $testversion The version to test
* @param bool $expected Whether the version meets the requirement
* @dataProvider meets_version_provider
* @covers ::meets_version
*/
public function test_meets_version(
string $version,
string $testversion,
bool $expected,
): void {
$instance = $this->get_mocked_instance_for_version($version);
$this->assertEquals($expected, $instance->meets_version($testversion));
}
/**
* Tests the requires_version method.
*
* @param string $version The version of the API to test against
* @param string $testversion The version to test
* @param bool $expected Whether the version meets the requirement
* @dataProvider meets_version_provider
* @covers ::requires_version
*/
public function test_requires_version(
string $version,
string $testversion,
bool $expected,
): void {
$instance = $this->get_mocked_instance_for_version($version);
if ($expected) {
$this->assertEmpty($instance->requires_version($testversion));
} else {
$this->expectException('moodle_exception');
$instance->requires_version($testversion);
}
}
/**
* Data provider for meets_version tests.
*
* @return array
*/
public static function meets_version_provider(): array {
return [
'Same version' => ['v1.1', '1.1', true],
'Same version latest' => ['v1.7', '1.7', true],
'Newer version rejected' => ['v1.1', '1.7', false],
'Older version accepted' => ['v1.7', '1.1', true],
];
}
/**
* Test the execute method with a command.
*
* @covers ::execute
*/
public function test_command_is_executed(): void {
$historycontainer = [];
$mock = new MockHandler();
$instance = $this->get_mocked_instance_for_version('v1.6', $historycontainer, $mock);
$command = new command(
$instance,
method: 'GET',
endpoint: 'test/endpoint',
params: [
'test' => 'test',
],
);
$mock->append(new Response(200));
$rc = new \ReflectionClass($instance);
$rcm = $rc->getMethod('execute');
$result = $rcm->invoke($instance, $command);
$this->assertEquals(200, $result->getStatusCode());
$this->assertCount(1, $historycontainer);
$request = array_shift($historycontainer);
$this->assertEquals('GET', $request['request']->getMethod());
$this->assertEquals('/test/endpoint', $request['request']->getUri()->getPath());
}
}
@@ -0,0 +1,169 @@
<?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;
use communication_matrix\local\spec\{v1p1, v1p2, v1p3, v1p4, v1p5, v1p6, v1p7};
use communication_matrix\tests\fixtures\mocked_matrix_client;
use core\http_client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Response;
/**
* A trait with shared tooling for handling matrix_client tests.
*
* @package communication_matrix
* @category test
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait matrix_client_test_trait {
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
// Ensure that the mocked client is available.
require_once(__DIR__ . '/fixtures/mocked_matrix_client.php');
}
public function setUp(): void {
parent::setUp();
// Reset the test client.
mocked_matrix_client::reset_client();
}
public function tearDown(): void {
parent::tearDown();
// Reset the test client.
mocked_matrix_client::reset_client();
}
/**
* Get a mocked instance for a specific Matrix API version,
*
* @param string $version
* @param array $historycontainer An array which will be filled with history for the mocked client.
* @param MockHandler|null $mock A MockHandler object that can be appended to
* @return matrix_client
*/
protected function get_mocked_instance_for_version(
string $version,
array &$historycontainer = [],
?MockHandler $mock = null,
): matrix_client {
if ($mock === null) {
$mock = new MockHandler();
}
// Add the version response.
$mock->append($this->get_mocked_version_response([$version]));
$handlerstack = HandlerStack::create($mock);
$history = Middleware::history($historycontainer);
$handlerstack->push($history);
$client = new http_client(['handler' => $handlerstack]);
mocked_matrix_client::set_client($client);
$client = mocked_matrix_client::instance(
'https://example.com',
'testtoken',
);
// Remove the request that is required to fetch the version from the history.
array_shift($historycontainer);
return $client;
}
/**
* Get a mocked response for the /versions well-known URI.
*
* @param array|null $versions
* @param array|null $unstablefeatures
* @return Response
*/
protected function get_mocked_version_response(
array $versions = null,
array $unstablefeatures = null,
): Response {
$data = (object) [
"versions" => array_values(self::get_current_versions()),
"unstable_features" => self::get_current_unstable_features(),
];
if ($versions) {
$data->versions = array_values($versions);
}
if ($unstablefeatures) {
$data->unstable_features = $unstablefeatures;
}
return new Response(200, [], json_encode($data));
}
/**
* A helper to get the current versions returned by synapse.
*
* @return array
*/
protected static function get_current_versions(): array {
return [
v1p1::class => "v1.1",
v1p2::class => "v1.2",
v1p3::class => "v1.3",
v1p4::class => "v1.4",
v1p5::class => "v1.5",
v1p6::class => "v1.6",
v1p7::class => "v1.7",
];
}
/**
* A helper to get the current unstable features returned by synapse.
* @return array
*/
protected static function get_current_unstable_features(): array {
return [
"org.matrix.label_based_filtering" => true,
"org.matrix.e2e_cross_signing" => true,
"org.matrix.msc2432" => true,
"uk.half-shot.msc2666.query_mutual_rooms" => true,
"io.element.e2ee_forced.public" => false,
"io.element.e2ee_forced.private" => false,
"io.element.e2ee_forced.trusted_private" => false,
"org.matrix.msc3026.busy_presence" => false,
"org.matrix.msc2285.stable" => true,
"org.matrix.msc3827.stable" => true,
"org.matrix.msc2716" => false,
"org.matrix.msc3440.stable" => true,
"org.matrix.msc3771" => true,
"org.matrix.msc3773" => false,
"fi.mau.msc2815" => false,
"fi.mau.msc2659.stable" => true,
"org.matrix.msc3882" => false,
"org.matrix.msc3881" => false,
"org.matrix.msc3874" => false,
"org.matrix.msc3886" => false,
"org.matrix.msc3912" => false,
"org.matrix.msc3952_intentional_mentions" => false,
"org.matrix.msc3981" => false,
"org.matrix.msc3391" => false,
];
}
}
@@ -0,0 +1,140 @@
<?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;
/**
* Tests for the matrix_room class.
*
* @package communication_matrix
* @category test
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \communication_matrix\matrix_room
*/
class matrix_room_test extends \advanced_testcase {
/**
* Test for load_by_processor_id with no record.
*
* @covers ::load_by_processor_id
*/
public function test_load_by_processor_id_none(): void {
$this->assertNull(matrix_room::load_by_processor_id(999999999));
}
/**
* Test for load_by_processor_id with valid records.
*
* @covers ::create_room_record
* @covers ::__construct
* @covers ::load_by_processor_id
* @covers ::get_processor_id
* @covers ::get_room_id
* @covers ::get_topic
*/
public function test_create_room_record(): void {
$this->resetAfterTest();
$room = matrix_room::create_room_record(
processorid: 10000,
topic: null,
);
$this->assertInstanceOf(matrix_room::class, $room);
$this->assertEquals(10000, $room->get_processor_id());
$this->assertNotNull('', $room->get_topic());
$this->assertEquals('', $room->get_topic());
$this->assertNull($room->get_room_id());
$room = matrix_room::create_room_record(
processorid: 12345,
topic: 'The topic of this room is thusly',
);
$this->assertInstanceOf(matrix_room::class, $room);
$this->assertEquals(12345, $room->get_processor_id());
$this->assertEquals('The topic of this room is thusly', $room->get_topic());
$this->assertNull($room->get_room_id());
$room = matrix_room::create_room_record(
processorid: 54321,
topic: 'The topic of this room is thusly',
roomid: 'This is a roomid',
);
$this->assertInstanceOf(matrix_room::class, $room);
$this->assertEquals(54321, $room->get_processor_id());
$this->assertEquals('The topic of this room is thusly', $room->get_topic());
$this->assertEquals('This is a roomid', $room->get_room_id());
$reloadedroom = matrix_room::load_by_processor_id(54321);
$this->assertEquals(54321, $reloadedroom->get_processor_id());
$this->assertEquals('The topic of this room is thusly', $reloadedroom->get_topic());
$this->assertEquals('This is a roomid', $reloadedroom->get_room_id());
}
/**
* Test for update_room_record.
*
* @covers ::update_room_record
*/
public function test_update_room_record(): void {
$this->resetAfterTest();
$room = matrix_room::create_room_record(
processorid: 12345,
topic: 'The topic of this room is that',
);
// Add a roomid.
$room->update_room_record(
roomid: 'This is a roomid',
);
$this->assertEquals('This is a roomid', $room->get_room_id());
$this->assertEquals('The topic of this room is that', $room->get_topic());
$this->assertEquals(12345, $room->get_processor_id());
// Alter the roomid and topic.
$room->update_room_record(
roomid: 'updatedRoomId',
topic: 'updatedTopic is here',
);
$this->assertEquals('updatedRoomId', $room->get_room_id());
$this->assertEquals('updatedTopic is here', $room->get_topic());
$this->assertEquals(12345, $room->get_processor_id());
}
/**
* Tests for delete_room_record.
*
* @covers ::delete_room_record
*/
public function test_delete_room_record(): void {
global $DB;
$this->resetAfterTest();
$room = matrix_room::create_room_record(
processorid: 12345,
topic: 'The topic of this room is that',
);
$this->assertCount(1, $DB->get_records('matrix_room'));
$room->delete_room_record();
$this->assertCount(0, $DB->get_records('matrix_room'));
}
}
@@ -0,0 +1,295 @@
<?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;
use core\context;
use GuzzleHttp\Psr7\Response;
/**
* Trait matrix_helper_trait to generate initial setup for matrix mock and associated helpers.
*
* @package communication_matrix
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @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
*/
protected string $accesstoken;
/**
* @var string $matrixhomeserverurl The server url of matrix synapse server
*/
protected string $matrixhomeserverurl;
/**
* Initialize the mock configs in settings.
*
* @return void
*/
protected function initialise_mock_configs(): void {
$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');
$admindata = json_decode($response->getBody());
$json = [
'identifier' => [
'type' => 'm.id.user',
'user' => $admindata->user_id,
],
'type' => 'm.login.password',
'password' => $admindata->password,
];
$request = $this->request($json);
$response = $request->post($this->matrixhomeserverurl . '/_matrix/client/r0/login');
$response = json_decode($response->getBody());
if (empty($response->access_token)) {
$this->markTestSkipped(
'The matrix mock server is not responsive, can not continue the tests'
);
}
$this->accesstoken = $response->access_token;
set_config('matrixaccesstoken', $this->accesstoken, 'communication_matrix');
}
/**
* Get the mock server url.
*
* @return string
*/
public function get_matrix_server_url(): string {
if (empty($this->matrixhomeserverurl)) {
throw new \coding_exception('Can not get this information without initializing the mock server.');
}
return $this->matrixhomeserverurl;
}
/**
* Get the matrix access token.
*
* @return string
*/
public function get_matrix_access_token(): string {
if (empty($this->accesstoken)) {
throw new \coding_exception('Can not get this information without initializing the mock server.');
}
return $this->accesstoken;
}
/**
* This test requires mock server to be present.
*
* @return void
*/
protected function initialise_mock_server(): void {
if (!defined('TEST_COMMUNICATION_MATRIX_MOCK_SERVER')) {
$this->markTestSkipped(
'The TEST_COMMUNICATION_MATRIX_MOCK_SERVER constant must be defined to run communication_matrix tests'
);
}
$this->reset_mock();
$this->initialise_mock_configs();
}
/**
* Get matrix room data from matrix server.
*
* @param string $roomid The id of the room
* @return \stdClass
*/
public function get_matrix_room_data(string $roomid): \stdClass {
$rooms = $this->backoffice_get_all_rooms();
foreach ($rooms as $room) {
if ($room->room_id === $roomid) {
return $room;
}
}
}
/**
* Get matrix user data from matrix server.
*
* @param string $roomid The id of the room
* @param string $matrixuserid The id of the user
* @return \stdClass
*/
public function get_matrix_user_data(string $roomid, string $matrixuserid): \stdClass {
$users = $this->backoffice_get_all_users();
foreach ($users as $user) {
if ($user->userid === $matrixuserid) {
return $user;
}
}
}
/**
* A backoffice call to get all registered users from our mock server.
*
* @return array
*/
public function backoffice_get_all_users(): array {
$client = new \core\http_client();
return json_decode($client->get($this->get_backoffice_uri('users'))->getBody())->users;
}
/**
* A backoffice method to create users and rooms on our mock server.
*
* @param array $users
* @param array $rooms
*/
public function backoffice_create_users_and_rooms(
array $users = [],
array $rooms = [],
): Response {
$client = new \core\http_client();
return $client->put(
$this->get_backoffice_uri('create'),
[
'json' => [
'users' => $users,
'rooms' => $rooms,
],
],
);
}
/**
* The http request for the api call.
*
* @param array $jsonarray The array of json
* @param array $headers The array of headers
* @return \core\http_client
*/
public function request(array $jsonarray = [], array $headers = []): \core\http_client {
$response = new \core\http_client([
'headers' => $headers,
'json' => $jsonarray,
]);
return $response;
}
/**
* Get the URI of a backoffice endpoint on the mock server.
*
* @param string $endpoint
* @return string
*/
protected function get_backoffice_uri(string $endpoint): string {
return $this->get_matrix_server_url() . '/backoffice/' . $endpoint;
}
/**
* Fetch all rooms from the back office.
*
* @return array
*/
public function backoffice_get_all_rooms(): array {
$client = new \core\http_client();
return json_decode($client->get($this->get_backoffice_uri('rooms'))->getBody())->rooms;
}
/**
* Return the first room from the server.
*
* In most cases there is only one room.
* @return \stdClass
*/
public function backoffice_get_room(): \stdClass {
// Fetch the room information from the server.
$rooms = $this->backoffice_get_all_rooms();
$this->assertCount(1, $rooms);
$room = reset($rooms);
return $room;
}
/**
* Reset the mock server
*
* @return void
*/
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 = json_decode($response->getBody());
if (empty($response->reset)) {
$this->markTestSkipped(
'The matrix mock server is not responsive, can not continue the tests'
);
}
}
}
/**
* Helper to create a room.
*
* @param null|string $component
* @param null|string $itemtype
* @param null|int $itemid
* @param null|string $roomname
* @param null|string $roomtopic
* @param null|stored_file $roomavatar
* @param array $members
* @return api
*/
protected function create_matrix_room(
?string $component = 'core_course',
?string $itemtype = 'example',
?int $itemid = 1,
?string $roomname = null,
?string $roomtopic = null,
?\stored_file $roomavatar = null,
array $members = [],
?context $context = null,
): \core_communication\api {
$context = $context ?? \core\context\system::instance();
// Create a new room.
$communication = \core_communication\api::load_by_instance(
context: $context,
component: $component,
instancetype: $itemtype,
instanceid: $itemid,
provider: 'communication_matrix',
);
$communication->create_and_configure_room(
communicationroomname: $roomname ?? 'Room name',
avatar: $roomavatar,
instance: (object) [
'matrixroomtopic' => $roomtopic ?? 'A fun topic',
],
);
$communication->add_members_to_room($members);
// Run the adhoc task.
$this->run_all_adhoc_tasks();
return \core_communication\api::load_by_instance(
context: $context,
component: $component,
instancetype: $itemtype,
instanceid: $itemid,
);
}
}
@@ -0,0 +1,239 @@
<?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;
use moodle_exception;
/**
* Class matrix_user_manager_test to test the matrix user manager.
*
* @package communication_matrix
* @category test
* @copyright 2023 Stevani Andolo <stevani.andolo@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \communication_matrix\matrix_user_manager
*/
class matrix_user_manager_test extends \advanced_testcase {
/**
* Test fetcihing a users matrix userid from Moodle.
*/
public function test_get_matrixid_from_moodle_without_field(): void {
$user = get_admin();
$this->assertNull(matrix_user_manager::get_matrixid_from_moodle($user->id));
}
/**
* Test fetching a user's matrix userid from Moodle.
*/
public function test_get_matrixid_from_moodle(): void {
$this->resetAfterTest();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
// Add user ids to both users.
matrix_user_manager::set_matrix_userid_in_moodle(
$user1->id,
'@someexampleuser:matrix.moodle.org',
);
matrix_user_manager::set_matrix_userid_in_moodle(
$user2->id,
'@someotherexampleuser:matrix.moodle.org',
);
// And confirm that they're fetched back.
$this->assertEquals(
'@someexampleuser:matrix.moodle.org',
matrix_user_manager::get_matrixid_from_moodle($user1->id),
);
$this->assertEquals(
'@someotherexampleuser:matrix.moodle.org',
matrix_user_manager::get_matrixid_from_moodle($user2->id),
);
}
/**
* Test fetching a formatted matrix userid from Moodle when no server is set.
*/
public function test_get_formatted_matrix_userid_unset(): void {
$this->expectException(moodle_exception::class);
matrix_user_manager::get_formatted_matrix_userid('No value');
}
/**
* Test fetch of a formatted matrix userid.
*
* @dataProvider get_formatted_matrix_userid_provider
* @param string $server
* @param string $username The moodle username to turn into a Matrix username
* @param string $expecteduserid The expected matrix user id
*/
public function test_get_formatted_matrix_userid(
string $server,
string $username,
string $expecteduserid,
): void {
$this->resetAfterTest();
set_config('matrixhomeserverurl', $server, 'communication_matrix');
$this->assertEquals(
$expecteduserid,
matrix_user_manager::get_formatted_matrix_userid($username),
);
}
/**
* Data provider for get_formatted_matrix_userid.
*
* @return array
*/
public static function get_formatted_matrix_userid_provider(): array {
return [
'alphanumeric' => [
'https://matrix.example.org',
'alphabet1',
'@alphabet1:matrix.example.org',
],
'chara' => [
'https://matrix.example.org',
'asdf#$%^&*()+{}|<>?!,asdf',
'@asdf.................asdf:matrix.example.org',
],
'local server' => [
'https://synapse',
'colin.creavey',
'@colin.creavey:synapse',
],
'server with port' => [
'https://matrix.example.org:8448',
'colin.creavey',
'@colin.creavey:matrix.example.org',
],
'numeric username' => [
'https://matrix.example.org',
'123456',
'@' . matrix_user_manager::MATRIX_USER_PREFIX . '123456:matrix.example.org',
],
];
}
/**
* Data provider for set_matrix_userid_in_moodle.
*
* @return array
*/
public static function set_matrix_userid_in_moodle_provider(): array {
return array_combine(
array_keys(self::get_formatted_matrix_userid_provider()),
array_map(
fn($value) => [$value[2]],
self::get_formatted_matrix_userid_provider(),
),
);
}
/**
* Test setting of a user's matrix userid in Moodle.
*
* @dataProvider set_matrix_userid_in_moodle_provider
* @param string $expectedusername
*/
public function test_set_matrix_userid_in_moodle(
string $expectedusername,
): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
matrix_user_manager::set_matrix_userid_in_moodle($user->id, $expectedusername);
// Get created matrixuserid from moodle.
$this->assertEquals(
$expectedusername,
matrix_user_manager::get_matrixid_from_moodle($user->id),
);
}
/**
* Test for getting a formatted matrix home server id.
*
* @dataProvider get_formatted_matrix_home_server_provider
* @param string $input
* @param string $expectedoutput
*/
public function test_get_formatted_matrix_home_server(
string $input,
string $expectedoutput
): void {
$this->resetAfterTest();
set_config(
'matrixhomeserverurl',
$input,
'communication_matrix',
);
$this->assertEquals(
$expectedoutput,
matrix_user_manager::get_formatted_matrix_home_server(),
);
}
/**
* Data provider for get_formatted_matrix_home_server.
*
* @return array
*/
public static function get_formatted_matrix_home_server_provider(): array {
return [
'www is removed' => [
'https://www.example.org',
'example.org',
],
'www is not removed if it is not at the beginning' => [
'https://matrix.www.example.org',
'matrix.www.example.org',
],
'others are not removed' => [
'https://matrix.example.org',
'matrix.example.org',
],
];
}
/**
* Test creation of matrix user profile fields.
*/
public function test_create_matrix_user_profile_fields(): void {
global $CFG;
require_once("{$CFG->dirroot}/user/profile/lib.php");
$this->resetAfterTest();
$matrixprofilefield = get_config('communication_matrix', 'matrixuserid_field');
$this->assertFalse($matrixprofilefield);
$this->assertIsString(matrix_user_manager::create_matrix_user_profile_fields());
$matrixprofilefield = get_config('communication_matrix', 'matrixuserid_field');
$this->assertNotFalse($matrixprofilefield);
$user = $this->getDataGenerator()->create_user();
$this->assertObjectHasProperty($matrixprofilefield, profile_user_record($user->id));
}
}
+30
View File
@@ -0,0 +1,30 @@
<?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/>.
/**
* Version information for communication_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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'communication_matrix';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_ALPHA;
+2
View File
@@ -0,0 +1,2 @@
This file describes API changes in /communication/provider/*
Information provided here is intended especially for developers.
+552
View File
@@ -0,0 +1,552 @@
<?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 core_communication\task\add_members_to_room_task;
use core_communication\task\create_and_configure_room_task;
use communication_matrix\matrix_test_helper_trait;
use core_communication\task\synchronise_provider_task;
use core_communication\task\synchronise_providers_task;
use core_communication\task\remove_members_from_room;
use core_communication\task\update_room_task;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../provider/matrix/tests/matrix_test_helper_trait.php');
require_once(__DIR__ . '/communication_test_helper_trait.php');
/**
* Class api_test to test the communication public api and its associated methods.
*
* @package core_communication
* @category test
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_communication\api
*/
class api_test extends \advanced_testcase {
use matrix_test_helper_trait;
use communication_test_helper_trait;
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setup_communication_configs();
$this->initialise_mock_server();
}
/**
* Test set data to the instance.
*/
public function test_set_data(): void {
$course = $this->get_course();
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
// Sample data.
$roomname = 'Sampleroom';
$provider = 'communication_matrix';
// Set the data.
$communication->set_data($course);
$roomnameidenfier = $communication->get_provider() . 'roomname';
// Test the set data.
$this->assertEquals($roomname, $course->$roomnameidenfier);
$this->assertEquals($provider, $course->selectedcommunication);
}
/**
* Test get_current_communication_provider method.
*/
public function test_get_provider(): void {
$course = $this->get_course();
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertEquals('communication_matrix', $communication->get_provider());
}
/**
* Test set_avatar method.
*/
public function test_set_avatar(): void {
global $CFG;
$this->setAdminUser();
$course = $this->get_course('Sampleroom', 'none');
// Sample data.
$communicationroomname = 'Sampleroom';
$selectedcommunication = 'communication_matrix';
$avatar = $this->create_communication_file(
'moodle_logo.jpg',
'moodle_logo.jpg',
);
// Create the room, settingthe avatar.
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
provider: $selectedcommunication,
);
$communication->create_and_configure_room($communicationroomname, $avatar);
// Reload the communication processor.
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
// Compare result.
$this->assertEquals(
$avatar->get_contenthash(),
$communicationprocessor->get_avatar()->get_contenthash(),
);
}
/**
* Test the create_and_configure_room method to add/create tasks.
*/
public function test_create_and_configure_room(): void {
// Get the course by disabling communication so that we can create it manually calling the api.
$course = $this->get_course('Sampleroom', 'none');
// Sample data.
$communicationroomname = 'Sampleroom';
$selectedcommunication = 'communication_matrix';
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
provider: $selectedcommunication,
);
$communication->create_and_configure_room($communicationroomname);
// Test the tasks added.
$adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\create_and_configure_room_task');
$this->assertCount(1, $adhoctask);
$adhoctask = reset($adhoctask);
$this->assertInstanceOf('\\core_communication\\task\\create_and_configure_room_task', $adhoctask);
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertEquals($communicationroomname, $communicationprocessor->get_room_name());
$this->assertEquals($selectedcommunication, $communicationprocessor->get_provider());
}
/**
* Test the create_and_configure_room method to add/create tasks when no communication provider selected.
*/
public function test_create_and_configure_room_without_communication_provider_selected(): void {
// Get the course by disabling communication so that we can create it manually calling the api.
$course = $this->get_course('Sampleroom', 'none');
// Test the tasks added.
$adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\create_and_configure_room_task');
$this->assertCount(0, $adhoctask);
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertNull($communicationprocessor);
}
/**
* Test update operation.
*/
public function test_update_room(): void {
$course = $this->get_course();
// Sample data.
$communicationroomname = 'Sampleroomupdated';
$selectedcommunication = 'communication_matrix';
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$communication->update_room(processor::PROVIDER_ACTIVE, $communicationroomname);
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertEquals($communicationroomname, $communicationprocessor->get_room_name());
$this->assertEquals($selectedcommunication, $communicationprocessor->get_provider());
$this->assertTrue($communicationprocessor->is_instance_active());
$communication->update_room(processor::PROVIDER_INACTIVE, $communicationroomname);
// Test updating active state.
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
provider: $selectedcommunication,
);
$this->assertEquals($communicationroomname, $communicationprocessor->get_room_name());
$this->assertEquals($selectedcommunication, $communicationprocessor->get_provider());
$this->assertFalse($communicationprocessor->is_instance_active());
}
/**
* Test delete operation.
*/
public function test_delete_room(): void {
$course = $this->get_course();
// Sample data.
$communicationroomname = 'Sampleroom';
$selectedcommunication = 'communication_matrix';
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertEquals($communicationroomname, $communicationprocessor->get_room_name());
$this->assertEquals($selectedcommunication, $communicationprocessor->get_provider());
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$communication->delete_room();
// Test the tasks added.
$adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\delete_room_task');
// Should be 2 as one for create, another for update.
$this->assertCount(1, $adhoctask);
$adhoctask = reset($adhoctask);
$this->assertInstanceOf('\\core_communication\\task\\delete_room_task', $adhoctask);
}
/**
* Test the adding and removing of members from room.
*
* @covers ::add_members_to_room
* @covers ::remove_members_from_room
*/
public function test_adding_and_removing_of_room_membership(): void {
$course = $this->get_course();
$userid = $this->get_user()->id;
// First test the adding members to a room.
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$communication->add_members_to_room([$userid]);
// Test the tasks added.
$adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\add_members_to_room_task');
$this->assertCount(1, $adhoctask);
// Now test the removing members from a room.
$communication->remove_members_from_room([$userid]);
// Test the tasks added.
$adhoctask = \core\task\manager::get_adhoc_tasks('\\core_communication\\task\\remove_members_from_room');
$this->assertCount(1, $adhoctask);
}
/**
* 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);
}
/**
* Test sync_provider method for the sync of available provider.
*
* @covers ::sync_provider
*/
public function test_sync_provider(): void {
// Generate the data.
$user = $this->getDataGenerator()->create_user();
$course1 = $this->get_course();
$this->getDataGenerator()->enrol_user($user->id, $course1->id);
$course2 = $this->get_course();
$this->getDataGenerator()->enrol_user($user->id, $course2->id);
// Now run the task to add sync providers.
$this->execute_task(synchronise_providers_task::class);
$adhoctask = \core\task\manager::get_adhoc_tasks(synchronise_provider_task::class);
$this->assertCount(2, $adhoctask);
}
/**
* Test the removal of all members from the room.
*
* @covers ::remove_all_members_from_room
*/
public function test_remove_all_members_from_room(): void {
$course = $this->get_course();
$userid = $this->get_user()->id;
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$communication->add_members_to_room([$userid]);
// Now test the removing members from a room.
$communication->remove_all_members_from_room();
// Test the remove members tasks added.
$adhoctask = \core\task\manager::get_adhoc_tasks(remove_members_from_room::class);
$this->assertCount(1, $adhoctask);
}
/**
* Test the configuration of room changes as well as the membership with the change of provider.
*
* @covers ::configure_room_and_membership_by_provider
*/
public function test_configure_room_and_membership_by_provider(): void {
global $DB;
$course = $this->get_course('Sampleroom', 'none');
$userid = $this->get_user()->id;
$provider = 'communication_matrix';
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
$communication->reload();
// Test that the task to create a room is added.
$adhoctask = \core\task\manager::get_adhoc_tasks(create_and_configure_room_task::class);
$this->assertCount(1, $adhoctask);
// Test that no update tasks are added.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(0, $adhoctask);
// Test that the task to add members to room is not added, as we are adding the user mapping not the task.
$adhoctask = \core\task\manager::get_adhoc_tasks(add_members_to_room_task::class);
$this->assertCount(0, $adhoctask);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now disable the provider by setting none.
$communication->configure_room_and_membership_by_provider(
provider: processor::PROVIDER_NONE,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
$communication->reload();
// Test that the task to delete a room is added.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
// Test that the task to remove members from room is added.
$adhoctask = \core\task\manager::get_adhoc_tasks(remove_members_from_room::class);
$this->assertCount(1, $adhoctask);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now try to set the same none provider again.
$communication->configure_room_and_membership_by_provider(
provider: processor::PROVIDER_NONE,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
// Test that no communicaiton task is added.
$adhoctask = \core\task\manager::get_adhoc_tasks(create_and_configure_room_task::class);
$this->assertCount(0, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(0, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(add_members_to_room_task::class);
$this->assertCount(0, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(remove_members_from_room::class);
$this->assertCount(0, $adhoctask);
// Now let's change it back to matrix and test the update task is added.
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
$communication->reload();
// Test create task is not added because communication has been created in the past.
$adhoctask = \core\task\manager::get_adhoc_tasks(create_and_configure_room_task::class);
$this->assertCount(0, $adhoctask);
// Test an update task added.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
// Test add membership task is added.
$adhoctask = \core\task\manager::get_adhoc_tasks(add_members_to_room_task::class);
$this->assertCount(1, $adhoctask);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
$course->customlinkurl = $course->customlinkurl ?? 'https://moodle.org';
// Now change the provider to another one.
$communication->configure_room_and_membership_by_provider(
provider: 'communication_customlink',
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
$communication->reload();
// Remove membership and update room task for the previous provider.
// Create room task for new one.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(remove_members_from_room::class);
$this->assertCount(1, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(create_and_configure_room_task::class);
$this->assertCount(1, $adhoctask);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now disable the provider.
$communication->configure_room_and_membership_by_provider(
provider: processor::PROVIDER_NONE,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
$communication->reload();
// Should have one update and one remove task.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
// This provider doesn't have any membership, so no remove task.
$adhoctask = \core\task\manager::get_adhoc_tasks(remove_members_from_room::class);
$this->assertCount(0, $adhoctask);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now enable the same provider again.
$communication->configure_room_and_membership_by_provider(
provider: $provider,
instance: $course,
communicationroomname: $course->fullname,
users: [$userid],
);
// Now it should have one update and one add task.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
$adhoctask = \core\task\manager::get_adhoc_tasks(add_members_to_room_task::class);
$this->assertCount(1, $adhoctask);
}
}
@@ -0,0 +1,50 @@
<?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/>.
// phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
require_once(__DIR__ . '/../../tests/communication_test_helper_trait.php');
/**
* Class behat_communication for behat custom steps and configuration for communication api.
*
* @package core_communication
* @category test
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @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;
/**
* Configure and enable communication experimental feature.
*
* @Given /^I enable communication experimental feature$/
*/
public function enable_communication_experimental_feature(): void {
$this->setup_communication_configs();
}
/**
* Disable communication experimental feature.
*
* @Given /^I disable communication experimental feature$/
*/
public function disable_communication_experimental_feature(): void {
$this->disable_communication_configs();
}
}
@@ -0,0 +1,109 @@
@communication
Feature: Access the communication configuration page
As an editing teacher
See dynamic form fields based on selected provider
Background: Set up teachers and course for the communication confifiguration page
Given I enable communication experimental feature
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| teacher2 | Teacher | 2 | teacher2@example.com |
And the following "courses" exist:
| fullname | shortname | category | selectedcommunication |
| Test course | Test course | 0 | none |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | Test course | editingteacher |
| teacher2 | Test course | teacher |
Scenario: A teacher with the correct capability can access the communication configuration page
Given I am on the "Test course" "Course" page logged in as "teacher1"
When I navigate to "Communication" in current page administration
Then I should see "Communication"
Scenario: A teacher without the correct capability cannot access the communication configuration page
Given I am on the "Test course" "Course" page logged in as "teacher2"
Then "Communication" "link" should not exist in current page administration
Scenario: I cannot see the communication link when communication provider is disabled
Given I disable communication experimental feature
And I am on the "Test course" "Course" page logged in as "teacher1"
Then "Communication" "link" should not exist in current page administration
@javascript
Scenario: The communication form fields toggle dynamically when valid provider is set
Given a Matrix mock server is configured
And I am on the "Test course" "Course" page logged in as "teacher1"
When I navigate to "Communication" in current page administration
And I set the following fields to these values:
| selectedcommunication | communication_matrix |
And I wait to be redirected
Then I should see "Room name"
And I should see "Room topic"
@javascript
Scenario: Changing the communication provider in the form fetches the correct data
Given a Matrix mock server is configured
And I am on the "Test course" "Course" page logged in as "teacher1"
When I navigate to "Communication" in current page administration
And I set the following fields to these values:
| selectedcommunication | communication_matrix |
And I wait to be redirected
And I should see "Room name"
And I should see "Room topic"
And I set the following fields to these values:
| communication_matrixroomname | Matrix room |
| matrixroomtopic | Matrix topic |
And I click on "Save changes" "button"
And I navigate to "Communication" in current page administration
Then the field "Room name" matches value "Matrix room"
And the field "Room topic" matches value "Matrix topic"
And I set the following fields to these values:
| selectedcommunication | communication_customlink |
And I wait to be redirected
And I should see "Room name"
And I should not see "Room topic"
And I should see "Custom link URL"
And I set the following fields to these values:
| communication_customlinkroomname | Custom link room |
| customlinkurl | https://moodle.org |
And I click on "Save changes" "button"
And I navigate to "Communication" in current page administration
And the field "Room name" matches value "Custom link room"
And the field "Custom link URL" matches value "https://moodle.org"
And I set the following fields to these values:
| selectedcommunication | communication_matrix |
And I wait to be redirected
And I should see "Room name"
And I should see "Room topic"
And the field "Room name" matches value "Matrix room"
And the field "Room topic" matches value "Matrix topic"
And I should not see "Custom link URL"
And I set the following fields to these values:
| selectedcommunication | communication_customlink |
And I wait to be redirected
And I should see "Room name"
And I should see "Custom link URL"
And the field "Room name" matches value "Custom link room"
And the field "Custom link URL" matches value "https://moodle.org"
And I should not see "Room topic"
And I set the following fields to these values:
| selectedcommunication | communication_matrix |
And I wait to be redirected
And I click on "Save changes" "button"
And I am on "Test course" course homepage with editing mode on
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| Group mode | Separate groups |
And I press "Save and display"
And I navigate to "Communication" in current page administration
And the field "Room name" matches value "Matrix room"
And the field "Room topic" matches value "Matrix topic"
And I press "Cancel"
And I navigate to "Settings" in current page administration
And I set the following fields to these values:
| Group mode | Visible groups |
And I navigate to "Communication" in current page administration
And the field "Room name" matches value "Matrix room"
And the field "Room topic" matches value "Matrix topic"
@@ -0,0 +1,131 @@
<?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;
/**
* Trait communication_test_helper_trait to generate initial setup for communication providers.
*
* @package core_communication
* @category test
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait communication_test_helper_trait {
/**
* Setup necessary configs for communication subsystem.
*
* @return void
*/
protected function setup_communication_configs(): void {
set_config('enablecommunicationsubsystem', 1);
}
/**
* Disable configs for communication subsystem.
*
* @return void
*/
protected function disable_communication_configs(): void {
set_config('enablecommunicationsubsystem', 0);
}
/**
* Get or create course if it does not exist
*
* @param string $roomname The room name for the communication api
* @param string $provider The selected provider
* @return \stdClass
*/
protected function get_course(
string $roomname = 'Sampleroom',
string $provider = 'communication_matrix',
array $extrafields = [],
): \stdClass {
$this->setup_communication_configs();
$records = [
'selectedcommunication' => $provider,
'communicationroomname' => $roomname,
];
return $this->getDataGenerator()->create_course(array_merge($records, $extrafields));
}
/**
* Get or create user if it does not exist.
*
* @param string $firstname The user's firstname for the communication api
* @param string $lastname The user's lastname for the communication api
* @param string $username The user's username for the communication api
* @return \stdClass
*/
protected function get_user(
string $firstname = 'Samplefn',
string $lastname = 'Sampleln',
string $username = 'sampleun'
): \stdClass {
$this->setup_communication_configs();
$records = [
'firstname' => $firstname,
'lastname' => $lastname,
'username' => $username,
];
return $this->getDataGenerator()->create_user($records);
}
/**
* Create a stored_file in a draft file area from a fixture file.
*
* @param string $filename The file name within the communication/tests/fixtures folder.
* @param string $storedname The name to use in the database.
* @return \stored_file
*/
protected function create_communication_file(
string $filename,
string $storedname,
): \stored_file {
global $CFG;
$fs = get_file_storage();
$itemid = file_get_unused_draft_itemid();
return $fs->create_file_from_pathname((object) [
'contextid' => \context_system::instance()->id,
'component' => 'user',
'filearea' => 'draftfile',
'itemid' => $itemid,
'filepath' => '/',
'filename' => $storedname,
], "{$CFG->dirroot}/communication/tests/fixtures/{$filename}");
}
/**
* Helper to execute a particular task.
*
* @param string $task The task.
*/
private function execute_task(string $task): void {
// Run the scheduled task.
ob_start();
$task = \core\task\manager::get_scheduled_task($task);
$task->execute();
ob_end_clean();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

+234
View File
@@ -0,0 +1,234 @@
<?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 communication_matrix\matrix_test_helper_trait;
use core_communication\processor as communication_processor;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../provider/matrix/tests/matrix_test_helper_trait.php');
require_once(__DIR__ . '/communication_test_helper_trait.php');
/**
* Test communication helper methods.
*
* @package core_communication
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_communication\helper
*/
class helper_test extends \advanced_testcase {
use communication_test_helper_trait;
use matrix_test_helper_trait;
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setup_communication_configs();
$this->initialise_mock_server();
}
/**
* Test load_by_group.
*/
public function test_load_by_group(): void {
// As communication is created by default.
$course = $this->get_course(
extrafields: ['groupmode' => SEPARATEGROUPS],
);
$group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$context = \context_course::instance(courseid: $course->id);
$groupcommunication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $groupcommunication->get_processor(),
);
}
/**
* Test load_by_course.
*/
public function test_load_by_course(): void {
// As communication is created by default.
$course = $this->get_course();
$coursecontext = \context_course::instance(courseid: $course->id);
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $coursecommunication->get_processor(),
);
}
/**
* Test get_access_to_all_group_cap_users.
*/
public function test_get_users_has_access_to_all_groups(): void {
global $DB;
// Set up the data with course, group, user etc.
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$course = $this->get_course();
$coursecontext = \context_course::instance(courseid: $course->id);
// Enrol user1 as teacher.
$teacherrole = $DB->get_record(
table: 'role',
conditions: ['shortname' => 'manager'],
);
$this->getDataGenerator()->enrol_user(
userid: $user1->id,
courseid: $course->id,
);
role_assign(
roleid: $teacherrole->id,
userid: $user1->id,
contextid: $coursecontext->id,
);
// Enrol user2 as student.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user(
userid: $user2->id,
courseid: $course->id,
);
role_assign(
roleid: $studentrole->id,
userid: $user2->id,
contextid: $coursecontext->id,
);
$allgroupaccessusers = helper::get_users_has_access_to_all_groups(
userids: [$user1->id, $user2->id],
courseid: $course->id,
);
$this->assertContains(
needle: $user1->id,
haystack: $allgroupaccessusers,
);
$this->assertNotContains(
needle: $user2->id,
haystack: $allgroupaccessusers,
);
}
/**
* Test update_communication_room_membership.
*/
public function test_update_communication_room_membership(): void {
global $DB;
// Set up the data with course, group, user etc.
$user = $this->getDataGenerator()->create_user();
$course = $this->get_course();
$coursecontext = \context_course::instance(courseid: $course->id);
$teacherrole = $DB->get_record(
table: 'role',
conditions: ['shortname' => 'manager'],
);
$this->getDataGenerator()->enrol_user(
userid: $user->id,
courseid: $course->id,
);
role_assign(
roleid: $teacherrole->id,
userid:$user->id,
contextid: $coursecontext->id,
);
// Now remove members from room.
helper::update_course_communication_room_membership(
course: $course,
userids: [$user->id],
memberaction: 'remove_members_from_room',
);
// Now test that there is communication instances for the course and the user removed from that instance.
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
// Check the user is added for course communication instance.
$courseusers = $coursecommunication->get_processor()->get_all_delete_flagged_userids();
$courseusers = reset($courseusers);
$this->assertEquals(
expected: $user->id,
actual: $courseusers,
);
// Now add members to room.
helper::update_course_communication_room_membership(
course: $course,
userids: [$user->id],
memberaction: 'add_members_to_room',
);
$coursecommunication->reload();
// Check the user is added for course communication instance.
$courseusers = $coursecommunication->get_processor()->get_instance_userids();
$courseusers = reset($courseusers);
$this->assertEquals(
expected: $user->id,
actual: $courseusers,
);
// Now update membership.
helper::update_course_communication_room_membership(
course: $course,
userids: [$user->id],
memberaction: 'update_room_membership',
);
$coursecommunication->reload();
// Check the user is added for course communication instance.
$courseusers = $coursecommunication->get_processor()->get_instance_userids();
$courseusers = reset($courseusers);
$this->assertEquals(
expected: $user->id,
actual: $courseusers,
);
// Now try using invalid action.
$this->expectException('coding_exception');
$this->expectExceptionMessage('Invalid action provided.');
helper::update_course_communication_room_membership(
course: $course,
userids: [$user->id],
memberaction: 'a_funny_action',
);
}
/**
* Test format_group_room_name.
*/
public function test_format_group_room_name(): void {
$baseroomname = 'Course A';
$groupname = 'Group 1';
$formattedroomname = helper::format_group_room_name($baseroomname, $groupname);
// Check the room name is formatted as expected.
$this->assertEquals('Group 1 (Course A)', $formattedroomname);
}
}
+628
View File
@@ -0,0 +1,628 @@
<?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 communication_matrix\matrix_test_helper_trait;
use core_communication\task\add_members_to_room_task;
use core_communication\task\create_and_configure_room_task;
use core_communication\task\delete_room_task;
use core_communication\task\update_room_membership_task;
use core_communication\task\update_room_task;
use core_communication\processor as communication_processor;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../provider/matrix/tests/matrix_test_helper_trait.php');
require_once(__DIR__ . '/communication_test_helper_trait.php');
/**
* Test communication hook listeners.
*
* @package core_communication
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_communication\hook_listener
*/
class hook_listener_test extends \advanced_testcase {
use communication_test_helper_trait;
use matrix_test_helper_trait;
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setup_communication_configs();
$this->initialise_mock_server();
}
/**
* Test create_group_communication.
*/
public function test_create_update_delete_group_communication(): void {
global $DB;
$course = $this->get_course(
roomname: 'Test room name',
extrafields: ['groupmode' => SEPARATEGROUPS],
);
$coursecontext = \context_course::instance(courseid: $course->id);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
// Enrol user1 as teacher.
$teacherrole = $DB->get_record(
table: 'role',
conditions: ['shortname' => 'manager'],
);
$this->getDataGenerator()->enrol_user(
userid: $user1->id,
courseid: $course->id,
);
role_assign(
roleid: $teacherrole->id,
userid: $user1->id,
contextid: $coursecontext->id,
);
// Enrol user2 as student.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user(
userid: $user2->id,
courseid: $course->id,
);
role_assign(
roleid: $studentrole->id,
userid: $user2->id,
contextid: $coursecontext->id,
);
$group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$context = \context_course::instance($course->id);
$groupcommunication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $groupcommunication->get_processor(),
);
$this->assertEquals(
expected: $group->id,
actual: $groupcommunication->get_processor()->get_instance_id(),
);
// Task to create room should be added.
$adhoctask = \core\task\manager::get_adhoc_tasks(create_and_configure_room_task::class);
$this->assertCount(1, $adhoctask);
// Task to add members to room should not be there as the room is yet to be created.
$adhoctask = \core\task\manager::get_adhoc_tasks(add_members_to_room_task::class);
$this->assertCount(0, $adhoctask);
// Only users with access to all groups should be added to the room at this point.
$groupcommunicationusers = $groupcommunication->get_processor()->get_all_userids_for_instance();
$this->assertEquals(
expected: [$user1->id],
actual: $groupcommunicationusers,
);
// Now delete all the ad-hoc tasks.
$DB->delete_records('task_adhoc');
// Now cann the update group but don't change the group name.
groups_update_group($group);
// No task should be added as nothing changed.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(0, $adhoctask);
// Now change the group name.
$group->name = 'Changed group name';
groups_update_group($group);
// Now one task should be there to update the group room name.
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_task::class);
$this->assertCount(1, $adhoctask);
$groupcommunication->reload();
$this->assertEquals(
expected: 'Changed group name (Test room name)',
actual: $groupcommunication->get_processor()->get_room_name(),
);
// Now delete the group.
groups_delete_group($group->id);
$adhoctask = \core\task\manager::get_adhoc_tasks(delete_room_task::class);
$this->assertCount(1, $adhoctask);
}
/**
* Test inactive users are not included when being mapped to a new communication instance.
*/
public function test_inactive_users_are_not_mapped_to_new_communication(): void {
// Create a course without a communication provider set.
$course = $this->getDataGenerator()->create_course();
// Enrol some users that are both active and inactive (suspended).
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
$user4 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user(
userid: $user1->id,
courseid: $course->id,
roleidorshortname: 'teacher',
);
$this->getDataGenerator()->enrol_user(
userid: $user2->id,
courseid: $course->id,
roleidorshortname: 'student',
);
$this->getDataGenerator()->enrol_user(
userid: $user3->id,
courseid: $course->id,
roleidorshortname: 'teacher',
status: ENROL_USER_SUSPENDED,
);
$this->getDataGenerator()->enrol_user(
userid: $user4->id,
courseid: $course->id,
roleidorshortname: 'student',
status: ENROL_USER_SUSPENDED,
);
// Set Matrix as the communication provider and update.
$course->selectedcommunication = 'communication_matrix';
$course->communication_matrixroomname = 'testroom';
update_course($course);
helper::update_course_communication_instance(
course: $course,
changesincoursecat: false,
);
// Load the communication instance and check that only the 2 active users are returned.
$communication = helper::load_by_course(
courseid: $course->id,
context: \context_course::instance($course->id),
);
$userids = $communication->get_processor()->get_all_userids_for_instance();
$this->assertEquals(
expected: 2,
actual: count($userids),
);
$this->assertContains(
needle: $user1->id,
haystack: $userids,
);
$this->assertContains(
needle: $user2->id,
haystack: $userids,
);
}
/**
* Test inactive users are not included when being mapped to a new communication instance using groups.
*/
public function test_inactive_users_are_not_mapped_to_new_group_communication(): void {
// Create a course without a communication provider set.
$course = $this->getDataGenerator()->create_course(
options: ['groupmode' => SEPARATEGROUPS],
);
// Enrol some users that are both active and inactive (suspended).
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
$user4 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user(
userid: $user1->id,
courseid: $course->id,
roleidorshortname: 'teacher',
);
$this->getDataGenerator()->enrol_user(
userid: $user2->id,
courseid: $course->id,
roleidorshortname: 'student',
);
$this->getDataGenerator()->enrol_user(
userid: $user3->id,
courseid: $course->id,
roleidorshortname: 'teacher',
status: ENROL_USER_SUSPENDED,
);
$this->getDataGenerator()->enrol_user(
userid: $user4->id,
courseid: $course->id,
roleidorshortname: 'student',
status: ENROL_USER_SUSPENDED,
);
// Create a group and add all users to it.
$group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
groups_add_member(
grouporid: $group,
userorid: $user1,
);
groups_add_member(
grouporid: $group,
userorid: $user2,
);
groups_add_member(
grouporid: $group,
userorid: $user3,
);
groups_add_member(
grouporid: $group,
userorid: $user4,
);
// Set Matrix as the communication provider and update.
$course->selectedcommunication = 'communication_matrix';
$course->communication_matrixroomname = 'testroom';
update_course($course);
helper::update_group_communication_instances_for_course(
course: $course,
provider: 'communication_matrix',
);
// Load the communication instance and check that only the 2 active users are returned.
$communication = helper::load_by_group(
groupid: $group->id,
context: \context_course::instance($course->id),
);
$userids = $communication->get_processor()->get_all_userids_for_instance();
$this->assertEquals(
expected: 2,
actual: count($userids),
);
$this->assertContains(
needle: $user1->id,
haystack: $userids,
);
$this->assertContains(
needle: $user2->id,
haystack: $userids,
);
}
/**
* Test add_members_to_group_room.
*/
public function test_add_members_to_group_room(): void {
global $DB;
$course = $this->get_course(
extrafields: ['groupmode' => SEPARATEGROUPS],
);
$coursecontext = \context_course::instance(courseid: $course->id);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
// Enrol user1 as teacher.
$teacherrole = $DB->get_record(
table: 'role',
conditions: ['shortname' => 'manager'],
);
$this->getDataGenerator()->enrol_user(
userid: $user1->id,
courseid: $course->id,
);
role_assign(
roleid: $teacherrole->id,
userid: $user1->id,
contextid: $coursecontext->id,
);
// Enrol user2 as student.
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$this->getDataGenerator()->enrol_user(
userid: $user2->id,
courseid: $course->id,
);
role_assign(
roleid: $studentrole->id,
userid: $user2->id,
contextid: $coursecontext->id,
);
$group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
// Now check if the teacher is added to the group room as the teacher has access to all groups.
$context = \context_course::instance($course->id);
$groupcommunication = helper::load_by_group(
groupid: $group->id,
context: $context,
);
// Now the communication instance should not have the student added yet.
$this->assertNotContains(
needle: $user2->id,
haystack: $groupcommunication->get_processor()->get_all_userids_for_instance(),
);
groups_add_member(
grouporid: $group,
userorid: $user2,
);
// Now it should have the student.
$this->assertContains(
needle: $user2->id,
haystack: $groupcommunication->get_processor()->get_all_userids_for_instance(),
);
}
/**
* Test if the course instances are created properly for course default provider.
*/
public function test_course_default_provider(): void {
$defaultprovider = 'communication_matrix';
// Set the default communication for course.
set_config(
name: 'coursecommunicationprovider',
value: $defaultprovider,
plugin: 'moodlecourse',
);
// Test that the default communication is created for course mode.
$course = $this->get_course();
$coursecontext = \context_course::instance(courseid: $course->id);
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$this->assertEquals(
expected: $defaultprovider,
actual: $coursecommunication->get_provider(),
);
$this->assertEquals(
expected: 'core_course',
actual: $coursecommunication->get_processor()->get_component(),
);
$this->assertEquals(
expected: $course->id,
actual: $coursecommunication->get_processor()->get_instance_id(),
);
}
/**
* Test update_course_communication.
*/
public function test_update_course_communication(): void {
global $DB;
// Set up the data with course, group, user etc.
$user = $this->getDataGenerator()->create_user();
$course = $this->get_course();
$group = $this->getDataGenerator()->create_group(record: ['courseid' => $course->id]);
$coursecontext = \context_course::instance(courseid: $course->id);
$teacherrole = $DB->get_record(
table: 'role',
conditions: ['shortname' => 'teacher'],
);
$this->getDataGenerator()->enrol_user(
userid: $user->id,
courseid: $course->id,
);
role_assign(
roleid: $teacherrole->id,
userid: $user->id,
contextid: $coursecontext->id,
);
groups_add_member(
grouporid: $group->id,
userorid: $user->id,
);
// Now test that there is communication instances for the course and the user added for that instance.
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $coursecommunication->get_processor(),
);
// Check the user is added for course communication instance.
$courseusers = $coursecommunication->get_processor()->get_all_userids_for_instance();
$courseusers = reset($courseusers);
$this->assertEquals(
expected: $user->id,
actual: $courseusers,
);
// Group should not have any instance yet.
$groupcommunication = helper::load_by_group(
groupid: $group->id,
context: $coursecontext,
);
$this->assertNull(actual: $groupcommunication->get_processor());
// Now update the course.
$course->groupmode = SEPARATEGROUPS;
$course->selectedcommunication = 'communication_matrix';
update_course(data: $course);
// Now there should be a group communication instance.
$groupcommunication->reload();
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $groupcommunication->get_processor(),
);
// The course communication instance must be active.
$coursecommunication->reload();
$this->assertInstanceOf(
expected: communication_processor::class,
actual: $coursecommunication->get_processor(),
);
// All the course instance users must be marked as deleted.
$coursecommunication->reload();
$courseusers = $coursecommunication->get_processor()->get_all_delete_flagged_userids();
$courseusers = reset($courseusers);
$this->assertEquals(
expected: $user->id,
actual: $courseusers,
);
// Group instance should have the user.
$groupusers = $groupcommunication->get_processor()->get_all_userids_for_instance();
$groupusers = reset($groupusers);
$this->assertEquals(
expected: $user->id,
actual: $groupusers,
);
// Now disable the communication instance for the course.
$course->selectedcommunication = communication_processor::PROVIDER_NONE;
update_course(data: $course);
// Now both course and group instance should be disabled.
$coursecommunication->reload();
$this->assertNull(actual: $coursecommunication->get_processor());
$groupcommunication->reload();
$this->assertNull(actual: $groupcommunication->get_processor());
}
/**
* Test create_course_communication_instance.
*/
public function test_create_course_communication_instance(): void {
$course = $this->get_course();
$coursecontext = \context_course::instance(courseid: $course->id);
$coursecommunication = helper::load_by_course(
courseid: $course->id,
context: $coursecontext,
);
$processor = $coursecommunication->get_processor();
$this->assertEquals(
expected: 'communication_matrix',
actual: $processor->get_provider(),
);
$this->assertEquals(
expected: 'Sampleroom',
actual: $processor->get_room_name(),
);
}
/**
* Test delete_course_communication.
*/
public function test_delete_course_communication(): void {
$course = $this->get_course();
delete_course(
courseorid: $course,
showfeedback: false,
);
$adhoctask = \core\task\manager::get_adhoc_tasks(delete_room_task::class);
$this->assertCount(1, $adhoctask);
}
/**
* Test update of room membership when user changes occur.
*/
public function test_update_user_room_memberships(): void {
global $DB;
$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);
role_assign($teacherrole->id, $user->id, $coursecontext->id);
$coursecommunication = helper::load_by_course($course->id, $coursecontext);
$courseusers = $coursecommunication->get_processor()->get_all_userids_for_instance();
$courseusers = reset($courseusers);
$this->assertEquals($user->id, $courseusers);
$user->suspended = 1;
user_update_user($user, false);
$coursecommunication->reload();
$courseusers = $coursecommunication->get_processor()->get_all_delete_flagged_userids();
$courseusers = reset($courseusers);
$this->assertEquals($user->id, $courseusers);
}
/**
* Test deletion of user room memberships when a user is deleted.
*/
public function test_delete_user_room_memberships(): void {
global $DB;
$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);
role_assign($teacherrole->id, $user->id, $coursecontext->id);
delete_user($user);
$coursecommunication = helper::load_by_course($course->id, $coursecontext);
$courseusers = $coursecommunication->get_processor()->get_all_userids_for_instance();
$this->assertEmpty($courseusers);
}
/**
* Test user room membership updates with role changes in a course.
*/
public function test_update_user_membership_for_role_changes(): void {
global $DB;
$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(update_room_membership_task::class);
$this->assertCount(1, $adhoctask);
role_assign($teacherrole->id, $user->id, $coursecontext->id);
$adhoctask = \core\task\manager::get_adhoc_tasks(update_room_membership_task::class);
$this->assertCount(2, $adhoctask);
}
}
+543
View File
@@ -0,0 +1,543 @@
<?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 communication_matrix\matrix_test_helper_trait;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../provider/matrix/tests/matrix_test_helper_trait.php');
require_once(__DIR__ . '/communication_test_helper_trait.php');
/**
* Class processor_test to test the communication internal api and its associated methods.
*
* @package core_communication
* @category test
* @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_communication\processor
*/
class processor_test extends \advanced_testcase {
use matrix_test_helper_trait;
use communication_test_helper_trait;
public function setUp(): void {
parent::setUp();
$this->resetAfterTest();
$this->setup_communication_configs();
$this->initialise_mock_server();
}
/**
* Test create instance.
*
* @covers ::create_instance
* @covers ::get_id
* @covers ::get_instance
* @covers ::get_room_name
*/
public function test_create_instance(): void {
global $DB;
$this->resetAfterTest();
// Sample test data.
$instanceid = 10;
$context = \core\context\system::instance();
$component = 'core';
$instancetype = 'mycommunication';
$selectedcommunication = 'communication_matrix';
$communicationroomname = 'communicationroom';
$communicationprocessor = processor::create_instance(
$context,
$selectedcommunication,
$instanceid,
$component,
$instancetype,
$communicationroomname,
);
// Now test the record against the database.
$communicationrecord = $DB->get_record(
'communication',
['instanceid' => $instanceid, 'component' => $component, 'instancetype' => $instancetype]
);
// Test against the set data.
$this->assertNotEmpty($communicationrecord);
$this->assertEquals($context->id, $communicationrecord->contextid);
$this->assertEquals($instanceid, $communicationrecord->instanceid);
$this->assertEquals($component, $communicationrecord->component);
$this->assertEquals($selectedcommunication, $communicationrecord->provider);
$this->assertEquals($communicationroomname, $communicationrecord->roomname);
$this->assertEquals($instancetype, $communicationrecord->instancetype);
// Test against the object.
$this->assertEquals($context->id, $communicationprocessor->get_context_id());
$this->assertEquals($context, $communicationprocessor->get_context());
$this->assertEquals($communicationprocessor->get_id(), $communicationrecord->id);
$this->assertEquals($communicationprocessor->get_provider(), $communicationrecord->provider);
$this->assertEquals($communicationprocessor->get_room_name(), $communicationrecord->roomname);
}
/**
* Test update instance.
*
* @covers ::update_instance
* @covers ::is_instance_active
* @covers ::get_id
* @covers ::get_room_name
*/
public function test_update_instance(): void {
global $DB;
$this->resetAfterTest();
// Sameple test data.
$instanceid = 10;
$context = \core\context\system::instance();
$component = 'core';
$instancetype = 'mycommunication';
$selectedcommunication = 'communication_matrix';
$communicationroomname = 'communicationroom';
$communicationprocessor = processor::create_instance(
$context,
$selectedcommunication,
$instanceid,
$component,
$instancetype,
$communicationroomname,
);
$selectedcommunication = 'none';
$communicationroomname = 'communicationroomedited';
$communicationprocessor->update_instance(processor::PROVIDER_INACTIVE, $communicationroomname);
// Now test the record against the database.
$communicationrecord = $DB->get_record('communication', [
'instanceid' => $instanceid,
'component' => $component,
'instancetype' => $instancetype,
]);
// Test against the set data.
$this->assertNotEmpty($communicationrecord);
$this->assertEquals($context->id, $communicationrecord->contextid);
$this->assertEquals($instanceid, $communicationrecord->instanceid);
$this->assertEquals($component, $communicationrecord->component);
$this->assertEquals(processor::PROVIDER_INACTIVE, $communicationrecord->active);
$this->assertEquals($communicationroomname, $communicationrecord->roomname);
$this->assertEquals($instancetype, $communicationrecord->instancetype);
// Test against the object.
$this->assertEquals($context->id, $communicationprocessor->get_context_id());
$this->assertEquals($context, $communicationprocessor->get_context());
$this->assertEquals($communicationprocessor->get_id(), $communicationrecord->id);
$this->assertEquals($communicationprocessor->is_instance_active(), $communicationrecord->active);
$this->assertEquals($communicationprocessor->get_room_name(), $communicationrecord->roomname);
}
/**
* Test delete instance.
*
* @covers ::delete_instance
* @covers ::create_instance
* @covers ::load_by_instance
*/
public function test_delete_instance(): void {
global $DB;
$this->resetAfterTest();
// Sameple test data.
$instanceid = 10;
$context = \core\context\system::instance();
$component = 'core';
$instancetype = 'mycommunication';
$selectedcommunication = 'communication_matrix';
$communicationroomname = 'communicationroom';
$communicationprocessor = processor::create_instance(
$context,
$selectedcommunication,
$instanceid,
$component,
$instancetype,
$communicationroomname,
);
$communicationprocessor->delete_instance();
// Now test the record against the database.
$communicationrecord = $DB->get_record('communication', [
'instanceid' => $instanceid,
'component' => $component,
'instancetype' => $instancetype,
]);
// Test against the set data.
$this->assertEmpty($communicationrecord);
// Test against the object.
$communicationprocessor = processor::load_by_instance(
context: $context,
component: $component,
instancetype: $instancetype,
instanceid: $instanceid,
);
$this->assertNull($communicationprocessor);
}
/**
* Test load by id.
*
* @covers ::load_by_instance
* @covers ::get_room_provider
*/
public function test_load_by_instance(): void {
$this->resetAfterTest();
$course = $this->get_course();
$context = \core\context\course::instance($course->id);
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: $context,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertNotNull($communicationprocessor);
$this->assertInstanceOf(communication_provider::class, $communicationprocessor->get_room_provider());
$this->assertInstanceOf(room_chat_provider::class, $communicationprocessor->get_room_provider());
$this->assertInstanceOf(room_user_provider::class, $communicationprocessor->get_room_provider());
$this->assertInstanceOf(user_provider::class, $communicationprocessor->get_room_provider());
}
/**
* Test load by id.
*
* @covers ::load_by_id
* @covers ::get_room_provider
* @covers ::load_by_instance
*/
public function test_load_by_id(): void {
$this->resetAfterTest();
$course = $this->get_course();
$context = \core\context\course::instance($course->id);
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: $context,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$communicationprocessorbyid = processor::load_by_id($communicationprocessor->get_id());
$this->assertNotNull($communicationprocessorbyid);
$this->assertInstanceOf(communication_provider::class, $communicationprocessorbyid->get_room_provider());
$this->assertInstanceOf(room_chat_provider::class, $communicationprocessorbyid->get_room_provider());
$this->assertInstanceOf(room_user_provider::class, $communicationprocessorbyid->get_room_provider());
$this->assertInstanceOf(user_provider::class, $communicationprocessorbyid->get_room_provider());
}
/**
* Test get component.
*
* @covers ::get_component
* @covers ::load_by_instance
*/
public function test_get_component(): void {
$this->resetAfterTest();
$course = $this->get_course();
$context = \core\context\course::instance($course->id);
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: $context,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertEquals('core_course', $communicationprocessor->get_component());
}
/**
* Test get provider.
*
* @covers ::get_provider
* @covers ::load_by_instance
*/
public function test_get_provider(): void {
$this->resetAfterTest();
$course = $this->get_course();
$context = \core\context\course::instance($course->id);
// Test the communication record exists when fetching the active provider.
$communicationprocessor = processor::load_by_instance(
context: $context,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertEquals('communication_matrix', $communicationprocessor->get_provider());
// Test the communication record exists when specifying the provider.
$communicationprocessor = processor::load_by_instance(
context: $context,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
provider: 'communication_matrix',
);
$this->assertEquals('communication_matrix', $communicationprocessor->get_provider());
// Test the communication record exists when the provider is not active.
$communicationprocessor->update_instance(processor::PROVIDER_INACTIVE);
$communicationprocessor = processor::load_by_instance(
context: $context,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
provider: 'communication_matrix',
);
$this->assertEquals('communication_matrix', $communicationprocessor->get_provider());
}
/**
* Test get room name.
*
* @covers ::get_room_name
* @covers ::load_by_instance
*/
public function test_get_room_name(): void {
$this->resetAfterTest();
$course = $this->get_course();
$context = \core\context\course::instance($course->id);
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: $context,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertEquals('Sampleroom', $communicationprocessor->get_room_name());
}
/**
* Test get room provider.
*
* @covers ::get_room_provider
* @covers ::require_room_features
* @covers ::supports_room_features
* @covers ::load_by_instance
*/
public function test_get_room_provider(): void {
$this->resetAfterTest();
$course = $this->get_course();
$context = \core\context\course::instance($course->id);
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: $context,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertInstanceOf(room_chat_provider::class, $communicationprocessor->get_room_provider());
}
/**
* Test get user provider.
*
* @covers ::get_user_provider
* @covers ::require_user_features
* @covers ::supports_user_features
* @covers ::load_by_instance
*/
public function test_get_user_provider(): void {
$this->resetAfterTest();
$course = $this->get_course();
$context = \core\context\course::instance($course->id);
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: $context,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertInstanceOf(user_provider::class, $communicationprocessor->get_room_provider());
}
/**
* Test get room user provider.
*
* @covers ::get_room_user_provider
* @covers ::require_room_features
* @covers ::require_room_user_features
* @covers ::supports_room_user_features
* @covers ::supports_room_features
* @covers ::load_by_instance
*/
public function test_get_room_user_provider(): void {
$this->resetAfterTest();
$course = $this->get_course();
$context = \core\context\course::instance($course->id);
// Test the communication record exists.
$communicationprocessor = processor::load_by_instance(
context: $context,
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertInstanceOf(room_user_provider::class, $communicationprocessor->get_room_user_provider());
}
/**
* Test get avatar.
*
* @covers ::get_avatar
* @covers ::load_by_instance
* @covers ::get_avatar_filename
* @covers ::set_avatar_filename
* @covers ::set_avatar_synced_flag
*/
public function test_get_avatar(): void {
$this->resetAfterTest();
$this->setAdminUser();
global $CFG;
$course = $this->get_course('Sampleroom', 'none');
// Sample data.
$communicationroomname = 'Sampleroom';
$selectedcommunication = 'communication_matrix';
$avatar = $this->create_communication_file(
'moodle_logo.jpg',
'moodle_logo.jpg',
);
$communication = \core_communication\api::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
provider: $selectedcommunication,
);
$communication->create_and_configure_room($communicationroomname, $avatar);
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$avatar = $communicationprocessor->get_avatar();
$this->assertNotNull($avatar);
$this->assertEquals($avatar->get_component(), 'core_communication');
$this->assertEquals($avatar->get_filearea(), 'avatar');
$this->assertEquals($avatar->get_itemid(), $communicationprocessor->get_id());
$this->assertEquals($avatar->get_filepath(), '/');
$this->assertEquals($avatar->get_filearea(), 'avatar');
$this->assertEquals($avatar->get_filename(), $communicationprocessor->get_avatar_filename());
// Change the avatar file name to something else and check it was set.
$communicationprocessor->set_avatar_filename('newname.svg');
$communicationprocessor = processor::load_by_instance(
context: \core\context\course::instance($course->id),
component: 'core_course',
instancetype: 'coursecommunication',
instanceid: $course->id,
);
$this->assertEquals($communicationprocessor->get_avatar_filename(), 'newname.svg');
}
/**
* Test if the provider is enabled and configured, or disabled.
*
* @covers ::is_provider_available
*/
public function test_is_provider_available(): void {
$this->resetAfterTest();
$communicationprovider = 'communication_matrix';
$this->assertTrue(processor::is_provider_available($communicationprovider));
// Now test is disabling the plugin returns false.
set_config('disabled', 1, $communicationprovider);
$this->assertFalse(processor::is_provider_available($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(
context: \core\context\course::instance($course->id),
component: $component,
instancetype: $instancetype,
instanceid: $course->id,
provider: $selectedcommunication,
);
$communication->create_and_configure_room($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(
context: \core\context\course::instance($course->id),
component: $component,
instancetype: $instancetype,
instanceid: $course->id,
);
$this->assertEquals([$user1], $communicationprocessor->get_all_delete_flagged_userids());
}
}
+6
View File
@@ -0,0 +1,6 @@
This file describes API changes in /communication/*
Information provided here is intended especially for developers.
=== 4.4.2 ===
* The get_enrolled_users_for_course() method now accepts an additional argument that can filter only active enrolments.