weekly release 4.5dev
[moodle.git] / communication / classes / api.php
bloba98592880fe6273e2ce639f4815359c9d61c8579
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 namespace core_communication;
19 use core\context;
20 use core_communication\task\add_members_to_room_task;
21 use core_communication\task\create_and_configure_room_task;
22 use core_communication\task\delete_room_task;
23 use core_communication\task\remove_members_from_room;
24 use core_communication\task\synchronise_provider_task;
25 use core_communication\task\update_room_task;
26 use core_communication\task\update_room_membership_task;
27 use stdClass;
29 /**
30 * Class api is the public endpoint of the communication api. This class is the point of contact for api usage.
32 * Communication api allows to add ad-hoc tasks to the queue to perform actions on the communication providers. This api will
33 * not allow any immediate actions to be performed on the communication providers. It will only add the tasks to the queue. The
34 * exception has been made for deletion of members in case of deleting the user. This is because the user will not be available.
35 * The member management api part allows run actions immediately if required.
37 * Communication api does allow to have form elements related to communication api in the required forms. This is done by using
38 * the form_definition method. This method will add the form elements to the form.
40 * @package core_communication
41 * @copyright 2023 Safat Shahin <safat.shahin@moodle.com>
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44 class api {
45 /**
46 * @var null|processor $communication The communication settings object
48 private ?processor $communication;
50 /**
51 * Communication handler constructor to manage and handle all communication related actions.
53 * This class is the entrypoint for all kinda usages.
54 * It will be used by the other api to manage the communication providers.
56 * @param context $context The context of the item for the instance
57 * @param string $component The component of the item for the instance
58 * @param string $instancetype The type of the item for the instance
59 * @param int $instanceid The id of the instance
60 * @param string|null $provider The provider type - if null will load for this context's active provider.
63 private function __construct(
64 private context $context,
65 private string $component,
66 private string $instancetype,
67 private int $instanceid,
68 private ?string $provider = null,
69 ) {
70 $this->communication = processor::load_by_instance(
71 context: $context,
72 component: $component,
73 instancetype: $instancetype,
74 instanceid: $instanceid,
75 provider: $provider,
79 /**
80 * Get the communication processor object.
82 * @param context $context The context of the item for the instance
83 * @param string $component The component of the item for the instance
84 * @param string $instancetype The type of the item for the instance
85 * @param int $instanceid The id of the instance
86 * @param string|null $provider The provider type - if null will load for this context's active provider.
87 * @return api
89 public static function load_by_instance(
90 context $context,
91 string $component,
92 string $instancetype,
93 int $instanceid,
94 ?string $provider = null,
95 ): self {
96 return new self(
97 context: $context,
98 component: $component,
99 instancetype: $instancetype,
100 instanceid: $instanceid,
101 provider: $provider,
106 * Reload in the internal instance data.
108 public function reload(): void {
109 $this->communication = processor::load_by_instance(
110 context: $this->context,
111 component: $this->component,
112 instancetype: $this->instancetype,
113 instanceid: $this->instanceid,
114 provider: $this->provider,
119 * Return the underlying communication processor object.
121 * @return ?processor
123 public function get_processor(): ?processor {
124 return $this->communication;
128 * Return the room provider.
130 * @return \core_communication\room_chat_provider
132 public function get_room_provider(): \core_communication\room_chat_provider {
133 return $this->communication->get_room_provider();
137 * Return the user provider.
139 * @return \core_communication\user_provider
141 public function get_user_provider(): \core_communication\user_provider {
142 return $this->communication->get_user_provider();
146 * Return the room user provider.
148 * @return \core_communication\room_user_provider
150 public function get_room_user_provider(): \core_communication\room_user_provider {
151 return $this->communication->get_room_user_provider();
155 * Return the form provider.
157 * @return \core_communication\form_provider
159 public function get_form_provider(): \core_communication\form_provider {
160 return $this->communication->get_form_provider();
164 * Check if the communication api is enabled.
166 public static function is_available(): bool {
167 return (bool) get_config('core', 'enablecommunicationsubsystem');
171 * Get the communication room url.
173 * @return string|null
175 public function get_communication_room_url(): ?string {
176 return $this->communication?->get_room_url();
180 * Get the list of plugins for form selection.
182 * @return array
184 public static function get_communication_plugin_list_for_form(): array {
185 // Add the option to have communication disabled.
186 $selection[processor::PROVIDER_NONE] = get_string('nocommunicationselected', 'communication');
187 $communicationplugins = \core\plugininfo\communication::get_enabled_plugins();
188 foreach ($communicationplugins as $pluginname => $notusing) {
189 $provider = 'communication_' . $pluginname;
190 if (processor::is_provider_available($provider)) {
191 $selection[$provider] = get_string('pluginname', 'communication_' . $pluginname);
194 return $selection;
198 * Get the enabled communication providers and default provider according to the selected provider.
200 * @param string|null $selecteddefaulprovider
201 * @return array
203 public static function get_enabled_providers_and_default(string $selecteddefaulprovider = null): array {
204 $communicationproviders = self::get_communication_plugin_list_for_form();
205 $defaulprovider = processor::PROVIDER_NONE;
206 if (!empty($selecteddefaulprovider) && array_key_exists($selecteddefaulprovider, $communicationproviders)) {
207 $defaulprovider = $selecteddefaulprovider;
209 return [$communicationproviders, $defaulprovider];
213 * Define the form elements for the communication api.
214 * This method will be called from the form definition method of the instance.
216 * @param \MoodleQuickForm $mform The form element
217 * @param string $selectdefaultcommunication The default selected communication provider in the form field
219 public function form_definition(
220 \MoodleQuickForm $mform,
221 string $selectdefaultcommunication = processor::PROVIDER_NONE
222 ): void {
223 global $PAGE;
225 [$communicationproviders, $defaulprovider] = self::get_enabled_providers_and_default($selectdefaultcommunication);
227 $PAGE->requires->js_call_amd('core_communication/providerchooser', 'init');
229 // List the communication providers.
230 $mform->addElement(
231 'select',
232 'selectedcommunication',
233 get_string('selectcommunicationprovider', 'communication'),
234 $communicationproviders,
235 ['data-communicationchooser-field' => 'selector'],
237 $mform->addHelpButton('selectedcommunication', 'selectcommunicationprovider', 'communication');
238 $mform->setDefault('selectedcommunication', $defaulprovider);
240 $mform->registerNoSubmitButton('updatecommunicationprovider');
241 $mform->addElement(
242 'submit',
243 'updatecommunicationprovider',
244 'update communication',
245 ['data-communicationchooser-field' => 'updateButton', 'class' => 'd-none']
248 // Just a placeholder for the communication options.
249 $mform->addElement('hidden', 'addcommunicationoptionshere');
250 $mform->setType('addcommunicationoptionshere', PARAM_BOOL);
254 * Set the form definitions for the plugins.
256 * @param \MoodleQuickForm $mform The moodle form
257 * @param string $provider The provider name
259 public function form_definition_for_provider(\MoodleQuickForm $mform, string $provider = processor::PROVIDER_NONE): void {
260 if ($provider === processor::PROVIDER_NONE) {
261 return;
264 // Room name for the communication provider.
265 $mform->insertElementBefore(
266 $mform->createElement(
267 'text',
268 $provider . 'roomname',
269 get_string('communicationroomname', 'communication'),
270 'maxlength="100" size="20"'
272 'addcommunicationoptionshere'
274 $mform->setType($provider . 'roomname', PARAM_TEXT);
276 $mform->insertElementBefore(
277 $mform->createElement(
278 'static',
279 'communicationroomnameinfo',
281 get_string('communicationroomnameinfo', 'communication'),
283 'addcommunicationoptionshere',
286 processor::set_provider_specific_form_definition($provider, $mform);
290 * Get the avatar file.
292 * @return null|\stored_file
294 public function get_avatar(): ?\stored_file {
295 $filename = $this->communication->get_avatar_filename();
296 if ($filename === null) {
297 return null;
299 $fs = get_file_storage();
300 $args = (array) $this->get_avatar_filerecord($filename);
301 return $fs->get_file(...$args) ?: null;
305 * Get the avatar file record for the avatar for filesystem.
307 * @param string $filename The filename of the avatar
308 * @return stdClass
310 protected function get_avatar_filerecord(string $filename): stdClass {
311 return (object) [
312 'contextid' => \core\context\system::instance()->id,
313 'component' => 'core_communication',
314 'filearea' => 'avatar',
315 'itemid' => $this->communication->get_id(),
316 'filepath' => '/',
317 'filename' => $filename,
322 * Get the avatar file.
324 * If null is set, then delete the old area file and set the avatarfilename to null.
325 * This will make sure the plugin api deletes the avatar from the room.
327 * @param null|\stored_file $avatar The stored file for the avatar
328 * @return bool
330 public function set_avatar(?\stored_file $avatar): bool {
331 $currentfilename = $this->communication->get_avatar_filename();
332 if ($avatar === null && empty($currentfilename)) {
333 return false;
336 $currentfilerecord = $this->get_avatar();
337 if ($avatar && $currentfilerecord) {
338 $currentfilehash = $currentfilerecord->get_contenthash();
339 $updatedfilehash = $avatar->get_contenthash();
341 // No update required.
342 if ($currentfilehash === $updatedfilehash) {
343 return false;
347 $context = \core\context\system::instance();
349 $fs = get_file_storage();
350 $fs->delete_area_files(
351 $context->id,
352 'core_communication',
353 'avatar',
354 $this->communication->get_id()
357 if ($avatar) {
358 $fs->create_file_from_storedfile(
359 $this->get_avatar_filerecord($avatar->get_filename()),
360 $avatar,
362 $this->communication->set_avatar_filename($avatar->get_filename());
363 } else {
364 $this->communication->set_avatar_filename(null);
367 // Indicate that we need to sync the avatar when the update task is run.
368 $this->communication->set_avatar_synced_flag(false);
370 return true;
374 * A helper to fetch the room name
376 * @return string
378 public function get_room_name(): string {
379 if (!$this->communication) {
380 return '';
382 return $this->communication->get_room_name();
386 * Set the form data if the data is already available.
388 * @param \stdClass $instance The instance object
390 public function set_data(\stdClass $instance): void {
391 if (!empty($instance->id) && $this->communication) {
392 $instance->selectedcommunication = $this->communication->get_provider();
393 $roomnameidentifier = $this->get_provider() . 'roomname';
394 $instance->$roomnameidentifier = $this->communication->get_room_name();
396 $this->communication->get_form_provider()->set_form_data($instance);
401 * Get the communication provider.
403 * @return string
405 public function get_provider(): string {
406 if (!$this->communication) {
407 return '';
409 return $this->communication->get_provider();
413 * Configure the room and membership by provider selected for the communication instance.
415 * This method will add a task to the queue to configure the room and membership by comparing the change of provider.
416 * There are some major cases to consider for this method to allow minimum duplication when this api is used.
417 * Some of the major cases are:
418 * 1. If the communication instance is not created at all, then create it and add members.
419 * 2. If the current provider is none and the new provider is also none, then nothing to do.
420 * 3. If the current and existing provider is the same, don't need to do anything.
421 * 4. If provider set to none, remove all the members.
422 * 5. If previous provider was not none and current provider is not none, but a different provider, remove members and add
423 * for the new one.
424 * 6. If previous provider was none and current provider is not none, don't need to remove, just
425 * update the selected provider and add users to that provider. Do not queue the task to add members to room as the room
426 * might not have created yet. The add room task adds the task to add members to room anyway.
427 * 7. If it's a new provider, never used/created, now create the room after considering all these cases for a new provider.
429 * @param string $provider The provider name
430 * @param \stdClass $instance The instance object
431 * @param string $communicationroomname The communication room name
432 * @param array $users The user ids to add to the room
433 * @param null|\stored_file $instanceimage The stored file for the avatar
434 * @param bool $queue Queue the task for the provider room or not
436 public function configure_room_and_membership_by_provider(
437 string $provider,
438 stdClass $instance,
439 string $communicationroomname,
440 array $users,
441 ?\stored_file $instanceimage = null,
442 bool $queue = true,
443 ): void {
444 // If the current provider is inactive and the new provider is also none, then nothing to do.
445 if (
446 $this->communication !== null &&
447 $this->communication->get_provider_status() === processor::PROVIDER_INACTIVE &&
448 $provider === processor::PROVIDER_NONE
450 return;
453 // If provider set to none, remove all the members.
454 if (
455 $this->communication !== null &&
456 $this->communication->get_provider_status() === processor::PROVIDER_ACTIVE &&
457 $provider === processor::PROVIDER_NONE
459 $this->remove_all_members_from_room();
460 $this->update_room(
461 active: processor::PROVIDER_INACTIVE,
462 communicationroomname: $communicationroomname,
463 avatar: $instanceimage,
464 instance: $instance,
465 queue: $queue,
467 return;
470 if (
471 // If previous provider was active and not none and current provider is not none, but a different provider,
472 // remove members and de-activate the previous provider.
473 $this->communication !== null &&
474 $this->communication->get_provider_status() === processor::PROVIDER_ACTIVE &&
475 $provider !== $this->get_provider()
477 $this->remove_all_members_from_room();
478 // Now deactivate the previous provider.
479 $this->update_room(
480 active: processor::PROVIDER_INACTIVE,
481 instance: $instance,
482 queue: $queue,
486 // Now re-init the constructor for the new provider.
487 $this->__construct(
488 context: $this->context,
489 component: $this->component,
490 instancetype: $this->instancetype,
491 instanceid: $this->instanceid,
492 provider: $provider,
495 // If it's a new provider, never used/created, now create the room.
496 if ($this->communication === null) {
497 $this->create_and_configure_room(
498 communicationroomname: $communicationroomname,
499 avatar: $instanceimage,
500 instance: $instance,
501 queue: $queue,
503 $queueusertask = false;
504 } else {
505 // Otherwise update the room.
506 $this->update_room(
507 active: processor::PROVIDER_ACTIVE,
508 communicationroomname: $communicationroomname,
509 avatar: $instanceimage,
510 instance: $instance,
511 queue: $queue,
513 $queueusertask = true;
516 // Now add the members.
517 $this->add_members_to_room(
518 userids: $users,
519 queue: $queueusertask,
525 * Create a communication ad-hoc task for create operation.
526 * This method will add a task to the queue to create the room.
528 * @param string $communicationroomname The communication room name
529 * @param null|\stored_file $avatar The stored file for the avatar
530 * @param \stdClass|null $instance The actual instance object
531 * @param bool $queue Whether to queue the task or not
533 public function create_and_configure_room(
534 string $communicationroomname,
535 ?\stored_file $avatar = null,
536 ?\stdClass $instance = null,
537 bool $queue = true,
538 ): void {
539 if ($this->provider === processor::PROVIDER_NONE || $this->provider === '') {
540 return;
542 // Create communication record.
543 $this->communication = processor::create_instance(
544 context: $this->context,
545 provider: $this->provider,
546 instanceid: $this->instanceid,
547 component: $this->component,
548 instancetype: $this->instancetype,
549 roomname: $communicationroomname,
552 // Update provider record from form data.
553 if ($instance !== null) {
554 $this->communication->get_form_provider()->save_form_data($instance);
557 // Set the avatar.
558 if (!empty($avatar)) {
559 $this->set_avatar($avatar);
562 // Nothing else to do if the queue is false.
563 if (!$queue) {
564 return;
567 // Add ad-hoc task to create the provider room.
568 create_and_configure_room_task::queue(
569 $this->communication,
574 * Create a communication ad-hoc task for update operation.
575 * This method will add a task to the queue to update the room.
577 * @param null|int $active The selected active state of the provider
578 * @param null|string $communicationroomname The communication room name
579 * @param null|\stored_file $avatar The stored file for the avatar
580 * @param \stdClass|null $instance The actual instance object
581 * @param bool $queue Whether to queue the task or not
583 public function update_room(
584 ?int $active = null,
585 ?string $communicationroomname = null,
586 ?\stored_file $avatar = null,
587 ?\stdClass $instance = null,
588 bool $queue = true,
589 ): void {
590 if (!$this->communication) {
591 return;
594 // If the provider is none, we don't need to do anything from room point of view.
595 if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
596 return;
599 $roomnamechange = null;
600 $activestatuschange = null;
602 // Check if the room name is being changed.
603 if (
604 $communicationroomname !== null &&
605 $communicationroomname !== $this->communication->get_room_name()
607 $roomnamechange = $communicationroomname;
610 // Check if the active status of the provider is being changed.
611 if (
612 $active !== null &&
613 $active !== $this->communication->is_instance_active()
615 $activestatuschange = $active;
618 if ($roomnamechange !== null || $activestatuschange !== null) {
619 $this->communication->update_instance(
620 active: $active,
621 roomname: $communicationroomname,
625 // Update provider record from form data.
626 if ($instance !== null) {
627 $this->communication->get_form_provider()->save_form_data($instance);
630 // Update the avatar.
631 // If the value is `null`, then unset the avatar.
632 $this->set_avatar($avatar);
634 // Nothing else to do if the queue is false.
635 if (!$queue) {
636 return;
639 // Always queue a room update, even if none of the above standard fields have changed.
640 // It is possible for providers to have custom fields that have been updated.
641 update_room_task::queue(
642 $this->communication,
647 * Create a communication ad-hoc task for delete operation.
648 * This method will add a task to the queue to delete the room.
650 public function delete_room(): void {
651 if ($this->communication !== null) {
652 // Add the ad-hoc task to remove the room data from the communication table and associated provider actions.
653 delete_room_task::queue(
654 $this->communication,
660 * Create a communication ad-hoc task for add members operation and add the user mapping.
662 * This method will add a task to the queue to add the room users.
664 * @param array $userids The user ids to add to the room
665 * @param bool $queue Whether to queue the task or not
667 public function add_members_to_room(array $userids, bool $queue = true): void {
668 // No communication object? something not done right.
669 if (!$this->communication) {
670 return;
673 // No user IDs or this provider does not manage users? No action required.
674 if (empty($userids) || !$this->communication->supports_user_features()) {
675 return;
678 $this->communication->create_instance_user_mapping($userids);
680 if ($queue) {
681 add_members_to_room_task::queue(
682 $this->communication
688 * Create a communication ad-hoc task for updating members operation and update the user mapping.
690 * This method will add a task to the queue to update the room users.
692 * @param array $userids The user ids to add to the room
693 * @param bool $queue Whether to queue the task or not
695 public function update_room_membership(array $userids, bool $queue = true): void {
696 // No communication object? something not done right.
697 if (!$this->communication) {
698 return;
701 // No userids? don't bother doing anything.
702 if (empty($userids)) {
703 return;
706 $this->communication->reset_users_sync_flag($userids);
708 if ($queue) {
709 update_room_membership_task::queue(
710 $this->communication
716 * Create a communication ad-hoc task for remove members operation or action immediately.
718 * This method will add a task to the queue to remove the room users.
720 * @param array $userids The user ids to remove from the room
721 * @param bool $queue Whether to queue the task or not
723 public function remove_members_from_room(array $userids, bool $queue = true): void {
724 // No communication object? something not done right.
725 if (!$this->communication) {
726 return;
729 $provider = $this->communication->get_provider();
731 if ($provider === processor::PROVIDER_NONE) {
732 return;
735 // No user IDs or this provider does not manage users? No action required.
736 if (empty($userids) || !$this->communication->supports_user_features()) {
737 return;
740 $this->communication->add_delete_user_flag($userids);
742 if ($queue) {
743 remove_members_from_room::queue(
744 $this->communication
750 * Remove all users from the room.
752 * @param bool $queue Whether to queue the task or not
754 public function remove_all_members_from_room(bool $queue = true): void {
755 // No communication object? something not done right.
756 if (!$this->communication) {
757 return;
760 if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
761 return;
764 // This provider does not manage users? No action required.
765 if (!$this->communication->supports_user_features()) {
766 return;
769 $this->communication->add_delete_user_flag($this->communication->get_all_userids_for_instance());
771 if ($queue) {
772 remove_members_from_room::queue(
773 $this->communication
779 * Display the communication room status notification.
781 public function show_communication_room_status_notification(): void {
782 // No communication, no room.
783 if (!$this->communication) {
784 return;
787 if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
788 return;
791 $roomstatus = $this->get_communication_room_url()
792 ? constants::COMMUNICATION_STATUS_READY
793 : constants::COMMUNICATION_STATUS_PENDING;
794 $pluginname = get_string('pluginname', $this->get_provider());
795 $message = get_string('communicationroom' . $roomstatus, 'communication', $pluginname);
797 // We only show the ready notification once per user.
798 // We check this with a custom user preference.
799 $roomreadypreference = "{$this->component}_{$this->instancetype}_{$this->instanceid}_room_ready";
801 switch ($roomstatus) {
802 case constants::COMMUNICATION_STATUS_PENDING:
803 \core\notification::add($message, \core\notification::INFO);
804 unset_user_preference($roomreadypreference);
805 break;
807 case constants::COMMUNICATION_STATUS_READY:
808 if (empty(get_user_preferences($roomreadypreference))) {
809 \core\notification::add($message, \core\notification::SUCCESS);
810 set_user_preference($roomreadypreference, true);
812 break;
817 * Add the task to sync the provider data with local Moodle data.
819 public function sync_provider(): void {
820 // No communication, return.
821 if (!$this->communication) {
822 return;
825 if ($this->communication->get_provider() === processor::PROVIDER_NONE) {
826 return;
829 synchronise_provider_task::queue(
830 $this->communication