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. 13 // 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/>. 16 17 namespace core_communication; 18 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\update_room_task; 25 use core_communication\task\update_room_membership_task; 26 use stdClass; 27 28 /** 29 * Class api is the public endpoint of the communication api. This class is the point of contact for api usage. 30 * 31 * Communication api allows to add ad-hoc tasks to the queue to perform actions on the communication providers. This api will 32 * not allow any immediate actions to be performed on the communication providers. It will only add the tasks to the queue. The 33 * exception has been made for deletion of members in case of deleting the user. This is because the user will not be available. 34 * The member management api part allows run actions immediately if required. 35 * 36 * Communication api does allow to have form elements related to communication api in the required forms. This is done by using 37 * the form_definition method. This method will add the form elements to the form. 38 * 39 * @package core_communication 40 * @copyright 2023 Safat Shahin <safat.shahin@moodle.com> 41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 42 */ 43 class api { 44 /** 45 * @var null|processor $communication The communication settings object 46 */ 47 private ?processor $communication; 48 49 /** 50 * Communication handler constructor to manage and handle all communication related actions. 51 * 52 * This class is the entrypoint for all kinda usages. 53 * It will be used by the other api to manage the communication providers. 54 * 55 * @param context $context The context of the item for the instance 56 * @param string $component The component of the item for the instance 57 * @param string $instancetype The type of the item for the instance 58 * @param int $instanceid The id of the instance 59 * @param string|null $provider The provider type - if null will load for this context's active provider. 60 * 61 */ 62 private function __construct( 63 private context $context, 64 private string $component, 65 private string $instancetype, 66 private int $instanceid, 67 private ?string $provider = null, 68 ) { 69 $this->communication = processor::load_by_instance( 70 context: $context, 71 component: $component, 72 instancetype: $instancetype, 73 instanceid: $instanceid, 74 provider: $provider, 75 ); 76 } 77 78 /** 79 * Get the communication processor object. 80 * 81 * @param context $context The context of the item for the instance 82 * @param string $component The component of the item for the instance 83 * @param string $instancetype The type of the item for the instance 84 * @param int $instanceid The id of the instance 85 * @param string|null $provider The provider type - if null will load for this context's active provider. 86 * @return api 87 */ 88 public static function load_by_instance( 89 context $context, 90 string $component, 91 string $instancetype, 92 int $instanceid, 93 ?string $provider = null, 94 ): self { 95 return new self( 96 context: $context, 97 component: $component, 98 instancetype: $instancetype, 99 instanceid: $instanceid, 100 provider: $provider, 101 ); 102 } 103 104 /** 105 * Reload in the internal instance data. 106 */ 107 public function reload(): void { 108 $this->communication = processor::load_by_instance( 109 context: $this->context, 110 component: $this->component, 111 instancetype: $this->instancetype, 112 instanceid: $this->instanceid, 113 provider: $this->provider, 114 ); 115 } 116 117 /** 118 * Return the underlying communication processor object. 119 * 120 * @return processor 121 */ 122 public function get_processor(): processor { 123 return $this->communication; 124 } 125 126 /** 127 * Return the room provider. 128 * 129 * @return \core_communication\room_chat_provider 130 */ 131 public function get_room_provider(): \core_communication\room_chat_provider { 132 return $this->communication->get_room_provider(); 133 } 134 135 /** 136 * Return the user provider. 137 * 138 * @return \core_communication\user_provider 139 */ 140 public function get_user_provider(): \core_communication\user_provider { 141 return $this->communication->get_user_provider(); 142 } 143 144 /** 145 * Return the room user provider. 146 * 147 * @return \core_communication\room_user_provider 148 */ 149 public function get_room_user_provider(): \core_communication\room_user_provider { 150 return $this->communication->get_room_user_provider(); 151 } 152 153 /** 154 * Return the form provider. 155 * 156 * @return \core_communication\form_provider 157 */ 158 public function get_form_provider(): \core_communication\form_provider { 159 return $this->communication->get_form_provider(); 160 } 161 162 /** 163 * Check if the communication api is enabled. 164 */ 165 public static function is_available(): bool { 166 return (bool) get_config('core', 'enablecommunicationsubsystem'); 167 } 168 169 /** 170 * Get the communication room url. 171 * 172 * @return string|null 173 */ 174 public function get_communication_room_url(): ?string { 175 return $this->communication?->get_room_url(); 176 } 177 178 /** 179 * Get the list of plugins for form selection. 180 * 181 * @return array 182 */ 183 public static function get_communication_plugin_list_for_form(): array { 184 // Add the option to have communication disabled. 185 $selection[processor::PROVIDER_NONE] = get_string('nocommunicationselected', 'communication'); 186 $communicationplugins = \core\plugininfo\communication::get_enabled_plugins(); 187 foreach ($communicationplugins as $pluginname => $notusing) { 188 $provider = 'communication_' . $pluginname; 189 if (processor::is_provider_available($provider)) { 190 $selection[$provider] = get_string('pluginname', 'communication_' . $pluginname); 191 } 192 } 193 return $selection; 194 } 195 196 /** 197 * Get the enabled communication providers and default provider according to the selected provider. 198 * 199 * @param string|null $selecteddefaulprovider 200 * @return array 201 */ 202 public static function get_enabled_providers_and_default(string $selecteddefaulprovider = null): array { 203 $communicationproviders = self::get_communication_plugin_list_for_form(); 204 $defaulprovider = processor::PROVIDER_NONE; 205 if (!empty($selecteddefaulprovider) && array_key_exists($selecteddefaulprovider, $communicationproviders)) { 206 $defaulprovider = $selecteddefaulprovider; 207 } 208 return [$communicationproviders, $defaulprovider]; 209 } 210 211 /** 212 * Define the form elements for the communication api. 213 * This method will be called from the form definition method of the instance. 214 * 215 * @param \MoodleQuickForm $mform The form element 216 * @param string $selectdefaultcommunication The default selected communication provider in the form field 217 */ 218 public function form_definition( 219 \MoodleQuickForm $mform, 220 string $selectdefaultcommunication = processor::PROVIDER_NONE 221 ): void { 222 global $PAGE; 223 224 [$communicationproviders, $defaulprovider] = self::get_enabled_providers_and_default($selectdefaultcommunication); 225 226 $PAGE->requires->js_call_amd('core_communication/providerchooser', 'init'); 227 228 // List the communication providers. 229 $mform->addElement( 230 'select', 231 'selectedcommunication', 232 get_string('selectcommunicationprovider', 'communication'), 233 $communicationproviders, 234 ['data-communicationchooser-field' => 'selector'], 235 ); 236 $mform->addHelpButton('selectedcommunication', 'selectcommunicationprovider', 'communication'); 237 $mform->setDefault('selectedcommunication', $defaulprovider); 238 239 $mform->registerNoSubmitButton('updatecommunicationprovider'); 240 $mform->addElement( 241 'submit', 242 'updatecommunicationprovider', 243 'update communication', 244 ['data-communicationchooser-field' => 'updateButton', 'class' => 'd-none'] 245 ); 246 247 // Just a placeholder for the communication options. 248 $mform->addElement('hidden', 'addcommunicationoptionshere'); 249 $mform->setType('addcommunicationoptionshere', PARAM_BOOL); 250 } 251 252 /** 253 * Set the form definitions for the plugins. 254 * 255 * @param \MoodleQuickForm $mform The moodle form 256 * @param string $provider The provider name 257 */ 258 public function form_definition_for_provider(\MoodleQuickForm $mform, string $provider = processor::PROVIDER_NONE): void { 259 if ($provider === processor::PROVIDER_NONE) { 260 return; 261 } 262 263 // Room name for the communication provider. 264 $mform->insertElementBefore( 265 $mform->createElement( 266 'text', 267 'communicationroomname', 268 get_string('communicationroomname', 'communication'), 269 'maxlength="100" size="20"' 270 ), 271 'addcommunicationoptionshere' 272 ); 273 $mform->setType('communicationroomname', PARAM_TEXT); 274 275 $mform->insertElementBefore( 276 $mform->createElement( 277 'static', 278 'communicationroomnameinfo', 279 '', 280 get_string('communicationroomnameinfo', 'communication'), 281 ), 282 'addcommunicationoptionshere', 283 ); 284 285 processor::set_provider_specific_form_definition($provider, $mform); 286 } 287 288 /** 289 * Get the avatar file. 290 * 291 * @return null|\stored_file 292 */ 293 public function get_avatar(): ?\stored_file { 294 $filename = $this->communication->get_avatar_filename(); 295 if ($filename === null) { 296 return null; 297 } 298 $fs = get_file_storage(); 299 $args = (array) $this->get_avatar_filerecord($filename); 300 return $fs->get_file(...$args) ?: null; 301 } 302 303 /** 304 * Get the avatar file record for the avatar for filesystem. 305 * 306 * @param string $filename The filename of the avatar 307 * @return stdClass 308 */ 309 protected function get_avatar_filerecord(string $filename): stdClass { 310 return (object) [ 311 'contextid' => \core\context\system::instance()->id, 312 'component' => 'core_communication', 313 'filearea' => 'avatar', 314 'itemid' => $this->communication->get_id(), 315 'filepath' => '/', 316 'filename' => $filename, 317 ]; 318 } 319 320 /** 321 * Get the avatar file. 322 * 323 * If null is set, then delete the old area file and set the avatarfilename to null. 324 * This will make sure the plugin api deletes the avatar from the room. 325 * 326 * @param null|\stored_file $avatar The stored file for the avatar 327 * @return bool 328 */ 329 public function set_avatar(?\stored_file $avatar): bool { 330 $currentfilename = $this->communication->get_avatar_filename(); 331 if ($avatar === null && empty($currentfilename)) { 332 return false; 333 } 334 335 $currentfilerecord = $this->get_avatar(); 336 if ($avatar && $currentfilerecord) { 337 $currentfilehash = $currentfilerecord->get_contenthash(); 338 $updatedfilehash = $avatar->get_contenthash(); 339 340 // No update required. 341 if ($currentfilehash === $updatedfilehash) { 342 return false; 343 } 344 } 345 346 $context = \core\context\system::instance(); 347 348 $fs = get_file_storage(); 349 $fs->delete_area_files( 350 $context->id, 351 'core_communication', 352 'avatar', 353 $this->communication->get_id() 354 ); 355 356 if ($avatar) { 357 $fs->create_file_from_storedfile( 358 $this->get_avatar_filerecord($avatar->get_filename()), 359 $avatar, 360 ); 361 $this->communication->set_avatar_filename($avatar->get_filename()); 362 } else { 363 $this->communication->set_avatar_filename(null); 364 } 365 366 // Indicate that we need to sync the avatar when the update task is run. 367 $this->communication->set_avatar_synced_flag(false); 368 369 return true; 370 } 371 372 /** 373 * A helper to fetch the room name 374 * 375 * @return string 376 */ 377 public function get_room_name(): string { 378 return $this->communication->get_room_name(); 379 } 380 381 /** 382 * Set the form data if the data is already available. 383 * 384 * @param \stdClass $instance The instance object 385 */ 386 public function set_data(\stdClass $instance): void { 387 if (!empty($instance->id) && $this->communication) { 388 $instance->selectedcommunication = $this->communication->get_provider(); 389 $instance->communicationroomname = $this->communication->get_room_name(); 390 391 $this->communication->get_form_provider()->set_form_data($instance); 392 } 393 } 394 395 /** 396 * Get the communication provider. 397 * 398 * @return string 399 */ 400 public function get_provider(): string { 401 if (!$this->communication) { 402 return ''; 403 } 404 return $this->communication->get_provider(); 405 } 406 407 /** 408 * Create a communication ad-hoc task for create operation. 409 * This method will add a task to the queue to create the room. 410 * 411 * @param string $communicationroomname The communication room name 412 * @param null|\stored_file $avatar The stored file for the avatar 413 * @param \stdClass|null $instance The actual instance object 414 */ 415 public function create_and_configure_room( 416 string $communicationroomname, 417 ?\stored_file $avatar = null, 418 ?\stdClass $instance = null, 419 ): void { 420 if ($this->provider === processor::PROVIDER_NONE || $this->provider === '') { 421 return; 422 } 423 424 // Create communication record. 425 $this->communication = processor::create_instance( 426 context: $this->context, 427 provider: $this->provider, 428 instanceid: $this->instanceid, 429 component: $this->component, 430 instancetype: $this->instancetype, 431 roomname: $communicationroomname, 432 ); 433 434 // Update provider record from form data. 435 if ($instance !== null) { 436 $this->communication->get_form_provider()->save_form_data($instance); 437 } 438 439 // Set the avatar. 440 if (!empty($avatar)) { 441 $this->set_avatar($avatar); 442 } 443 444 // Add ad-hoc task to create the provider room. 445 create_and_configure_room_task::queue( 446 $this->communication, 447 ); 448 } 449 450 /** 451 * Create a communication ad-hoc task for update operation. 452 * This method will add a task to the queue to update the room. 453 * 454 * @param null|int $active The selected active state of the provider 455 * @param null|string $communicationroomname The communication room name 456 * @param null|\stored_file $avatar The stored file for the avatar 457 * @param \stdClass|null $instance The actual instance object 458 */ 459 public function update_room( 460 ?int $active = null, 461 ?string $communicationroomname = null, 462 ?\stored_file $avatar = null, 463 ?\stdClass $instance = null, 464 ): void { 465 // If the provider is none, we don't need to do anything from room point of view. 466 if ($this->communication->get_provider() === processor::PROVIDER_NONE) { 467 return; 468 } 469 470 $roomnamechange = null; 471 $activestatuschange = null; 472 473 // Check if the room name is being changed. 474 if ( 475 $communicationroomname !== null && 476 $communicationroomname !== $this->communication->get_room_name() 477 ) { 478 $roomnamechange = $communicationroomname; 479 } 480 481 // Check if the active status of the provider is being changed. 482 if ( 483 $active !== null && 484 $active !== $this->communication->is_instance_active() 485 ) { 486 $activestatuschange = $active; 487 } 488 489 if ($roomnamechange !== null || $activestatuschange !== null) { 490 $this->communication->update_instance( 491 active: $active, 492 roomname: $communicationroomname, 493 ); 494 } 495 496 // Update provider record from form data. 497 if ($instance !== null) { 498 $this->communication->get_form_provider()->save_form_data($instance); 499 } 500 501 // Update the avatar. 502 // If the value is `null`, then unset the avatar. 503 $this->set_avatar($avatar); 504 505 // Always queue a room update, even if none of the above standard fields have changed. 506 // It is possible for providers to have custom fields that have been updated. 507 update_room_task::queue( 508 $this->communication, 509 ); 510 } 511 512 /** 513 * Create a communication ad-hoc task for delete operation. 514 * This method will add a task to the queue to delete the room. 515 */ 516 public function delete_room(): void { 517 if ($this->communication !== null) { 518 // Add the ad-hoc task to remove the room data from the communication table and associated provider actions. 519 delete_room_task::queue( 520 $this->communication, 521 ); 522 } 523 } 524 525 /** 526 * Create a communication ad-hoc task for add members operation and add the user mapping. 527 * 528 * This method will add a task to the queue to add the room users. 529 * 530 * @param array $userids The user ids to add to the room 531 * @param bool $queue Whether to queue the task or not 532 */ 533 public function add_members_to_room(array $userids, bool $queue = true): void { 534 // No communication object? something not done right. 535 if (!$this->communication) { 536 return; 537 } 538 539 // No user IDs or this provider does not manage users? No action required. 540 if (empty($userids) || !$this->communication->supports_user_features()) { 541 return; 542 } 543 544 $this->communication->create_instance_user_mapping($userids); 545 546 if ($queue) { 547 add_members_to_room_task::queue( 548 $this->communication 549 ); 550 } 551 } 552 553 /** 554 * Create a communication ad-hoc task for updating members operation and update the user mapping. 555 * 556 * This method will add a task to the queue to update the room users. 557 * 558 * @param array $userids The user ids to add to the room 559 * @param bool $queue Whether to queue the task or not 560 */ 561 public function update_room_membership(array $userids, bool $queue = true): void { 562 // No communication object? something not done right. 563 if (!$this->communication) { 564 return; 565 } 566 567 // No userids? don't bother doing anything. 568 if (empty($userids)) { 569 return; 570 } 571 572 $this->communication->reset_users_sync_flag($userids); 573 574 if ($queue) { 575 update_room_membership_task::queue( 576 $this->communication 577 ); 578 } 579 } 580 581 /** 582 * Create a communication ad-hoc task for remove members operation or action immediately. 583 * 584 * This method will add a task to the queue to remove the room users. 585 * 586 * @param array $userids The user ids to remove from the room 587 * @param bool $queue Whether to queue the task or not 588 */ 589 public function remove_members_from_room(array $userids, bool $queue = true): void { 590 // No communication object? something not done right. 591 if (!$this->communication) { 592 return; 593 } 594 595 $provider = $this->communication->get_provider(); 596 597 if ($provider === processor::PROVIDER_NONE) { 598 return; 599 } 600 601 // No user IDs or this provider does not manage users? No action required. 602 if (empty($userids) || !$this->communication->supports_user_features()) { 603 return; 604 } 605 606 $this->communication->add_delete_user_flag($userids); 607 608 if ($queue) { 609 remove_members_from_room::queue( 610 $this->communication 611 ); 612 } 613 } 614 615 /** 616 * Display the communication room status notification. 617 */ 618 public function show_communication_room_status_notification(): void { 619 // No communication, no room. 620 if (!$this->communication) { 621 return; 622 } 623 624 if ($this->communication->get_provider() === processor::PROVIDER_NONE) { 625 return; 626 } 627 628 $roomstatus = $this->get_communication_room_url() ? 'ready' : 'pending'; 629 $pluginname = get_string('pluginname', $this->get_provider()); 630 $message = get_string('communicationroom' . $roomstatus, 'communication', $pluginname); 631 632 switch ($roomstatus) { 633 case 'pending': 634 \core\notification::add($message, \core\notification::INFO); 635 break; 636 637 case 'ready': 638 // We only show the ready notification once per user. 639 // We check this with a custom user preference. 640 $roomreadypreference = "{$this->component}_{$this->instancetype}_{$this->instanceid}_room_ready"; 641 642 if (empty(get_user_preferences($roomreadypreference))) { 643 \core\notification::add($message, \core\notification::SUCCESS); 644 set_user_preference($roomreadypreference, true); 645 } 646 break; 647 } 648 } 649 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body