diff --git a/lib/plugins.json b/lib/plugins.json index e3e72c90a71..e8e9cddf445 100644 --- a/lib/plugins.json +++ b/lib/plugins.json @@ -446,7 +446,8 @@ "solr" ], "smsgateway": [ - "aws" + "aws", + "modica" ], "theme": [ "boost", diff --git a/sms/gateway/modica/classes/gateway.php b/sms/gateway/modica/classes/gateway.php new file mode 100644 index 00000000000..247e6835aa4 --- /dev/null +++ b/sms/gateway/modica/classes/gateway.php @@ -0,0 +1,88 @@ +. + +namespace smsgateway_modica; + +use core\http_client; +use core_sms\manager; +use core_sms\message; +use GuzzleHttp\Exception\GuzzleException; + +/** + * Modica SMS gateway. + * + * @see https://confluence.modicagroup.com/display/DC/Mobile+Gateway+REST+API#MobileGatewayRESTAPI-Sendingtoasingledestination + * @package smsgateway_modica + * @copyright 2025 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class gateway extends \core_sms\gateway { + + /** + * @var string MODICA_DEFAULT_API The default api gateway for modica. + */ + public const MODICA_DEFAULT_API = 'https://api.modicagroup.com/rest/gateway/messages'; + + #[\Override] + public function send( + message $message, + ): message { + $recipientnumber = manager::format_number( + phonenumber: $message->recipientnumber, + countrycode: $this->config->countrycode ?? null, + ); + $json = json_encode( + [ + 'destination' => $recipientnumber, + 'content' => $message->content, + ], + JSON_THROW_ON_ERROR, + ); + + $client = \core\di::get(http_client::class); + try { + $response = $client->post( + uri: $config->modica_url ?? self::MODICA_DEFAULT_API, + options: + [ + 'auth' => [ + $this->config->modica_application_name, + $this->config->modica_application_password, + ], + 'headers' => ['Content-Type' => 'application/json'], + 'body' => $json, + ], + ); + + if ($response->getStatusCode() === 201) { + $status = \core_sms\message_status::GATEWAY_SENT; + } else { + $status = \core_sms\message_status::GATEWAY_FAILED; + } + } catch (GuzzleException $e) { + $status = \core_sms\message_status::GATEWAY_FAILED; + } + + return $message->with( + status: $status, + ); + } + + #[\Override] + public function get_send_priority(message $message): int { + return 50; + } +} diff --git a/sms/gateway/modica/classes/hook_listener.php b/sms/gateway/modica/classes/hook_listener.php new file mode 100644 index 00000000000..7ab21ead8f1 --- /dev/null +++ b/sms/gateway/modica/classes/hook_listener.php @@ -0,0 +1,86 @@ +. + +namespace smsgateway_modica; + +use core_sms\hook\after_sms_gateway_form_hook; + +/** + * Hook listener for Modica sms gateway. + * + * @package smsgateway_modica + * @copyright 2025 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class hook_listener { + + /** + * Hook listener for the sms gateway setup form. + * + * @param after_sms_gateway_form_hook $hook The hook to add to sms gateway setup. + */ + public static function set_form_definition_for_modica_sms_gateway(after_sms_gateway_form_hook $hook): void { + if ($hook->plugin !== 'smsgateway_modica') { + return; + } + + $mform = $hook->mform; + + $mform->addElement('static', 'information', get_string('modica_information', 'smsgateway_modica')); + + $mform->addElement( + 'text', + 'modica_url', + get_string('modica_url', 'smsgateway_modica'), + 'maxlength="255" size="20"', + ); + $mform->setType('modica_url', PARAM_URL); + $mform->addRule('modica_url', get_string('maximumchars', '', 255), 'maxlength', 255); + $mform->addRule('modica_url', null, 'required'); + $mform->setDefault( + elementName: 'modica_url', + defaultValue: gateway::MODICA_DEFAULT_API, + ); + + $mform->addElement( + 'text', + 'modica_application_name', + get_string('modica_application_name', 'smsgateway_modica'), + 'maxlength="255" size="20"', + ); + $mform->setType('modica_application_name', PARAM_TEXT); + $mform->addRule('modica_application_name', get_string('maximumchars', '', 255), 'maxlength', 255); + $mform->addRule('modica_application_name', null, 'required'); + $mform->setDefault( + elementName: 'modica_application_name', + defaultValue: '', + ); + + $mform->addElement( + 'passwordunmask', + 'modica_application_password', + get_string('modica_application_password', 'smsgateway_modica'), + 'maxlength="255" size="20"', + ); + $mform->setType('modica_application_password', PARAM_TEXT); + $mform->addRule('modica_application_password', get_string('maximumchars', '', 255), 'maxlength', 255); + $mform->addRule('modica_application_password', null, 'required'); + $mform->setDefault( + elementName: 'modica_application_password', + defaultValue: '', + ); + } +} diff --git a/sms/gateway/modica/classes/privacy/provider.php b/sms/gateway/modica/classes/privacy/provider.php new file mode 100644 index 00000000000..25e883fbe30 --- /dev/null +++ b/sms/gateway/modica/classes/privacy/provider.php @@ -0,0 +1,35 @@ +. + +namespace smsgateway_modica\privacy; + +use core_privacy\local\metadata\null_provider; + +/** + * Privacy Subsystem for smsgateway_modica implementing null_provider. + * + * @package smsgateway_modica + * @copyright 2025 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @codeCoverageIgnore + */ +class provider implements null_provider { + + #[\Override] + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/sms/gateway/modica/db/hooks.php b/sms/gateway/modica/db/hooks.php new file mode 100644 index 00000000000..fd97630d336 --- /dev/null +++ b/sms/gateway/modica/db/hooks.php @@ -0,0 +1,32 @@ +. + +/** + * Hook listener callbacks for Modica sms gateway. + * + * @package smsgateway_modica + * @copyright 2025 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$callbacks = [ + [ + 'hook' => \core_sms\hook\after_sms_gateway_form_hook::class, + 'callback' => \smsgateway_modica\hook_listener::class . '::set_form_definition_for_modica_sms_gateway', + ], +]; diff --git a/sms/gateway/modica/lang/en/smsgateway_modica.php b/sms/gateway/modica/lang/en/smsgateway_modica.php new file mode 100644 index 00000000000..c58d3379819 --- /dev/null +++ b/sms/gateway/modica/lang/en/smsgateway_modica.php @@ -0,0 +1,30 @@ +. + +/** + * Strings for component smsgateway_modica, language 'en'. + * + * @package smsgateway_modica + * @copyright 2025 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['modica_application_name'] = 'Modica application name'; +$string['modica_application_password'] = 'Modica application password'; +$string['modica_information'] = 'Complete the following fields using the information provided by Modica'; +$string['modica_url'] = 'Modica API URL'; +$string['pluginname'] = 'Modica'; +$string['privacy:metadata'] = 'The Modica SMS gateway plugin does not store any personal data.'; diff --git a/sms/gateway/modica/tests/gateway_test.php b/sms/gateway/modica/tests/gateway_test.php new file mode 100644 index 00000000000..af85d65d192 --- /dev/null +++ b/sms/gateway/modica/tests/gateway_test.php @@ -0,0 +1,93 @@ +. + +namespace smsgateway_modica; + +use core_sms\message; +use core_sms\message_status; +use GuzzleHttp\Psr7\Response; + +/** + * Modica SMS gateway tests. + * + * @package smsgateway_modica + * @category test + * @copyright 2025 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \smsgateway_modica\gateway + */ +final class gateway_test extends \advanced_testcase { + + public function test_send(): void { + $this->resetAfterTest(); + + $config = new \stdClass(); + $config->modica_url = gateway::MODICA_DEFAULT_API; + $config->modica_application_name = 'test_application'; + $config->modica_application_password = 'test_password'; + + $manager = \core\di::get(\core_sms\manager::class); + $gw = $manager->create_gateway_instance( + classname: gateway::class, + name: 'modica', + enabled: true, + config: $config, + ); + + // Mock the http client to return a successful response. + ['mock' => $mock] = $this->get_mocked_http_client(); + + // Append the response status. + $mock->append(new Response( + status: 201, + )); + + $message = $manager->send( + recipientnumber: '+447123456789', + content: 'Hello, world!', + component: 'core', + messagetype: 'test', + recipientuserid: null, + async: false, + ); + + $this->assertInstanceOf(message::class, $message); + $this->assertIsInt($message->id); + $this->assertEquals(message_status::GATEWAY_SENT, $message->status); + $this->assertEquals($gw->id, $message->gatewayid); + $this->assertEquals('Hello, world!', $message->content); + + $storedmessage = $manager->get_message(['id' => $message->id]); + $this->assertEquals($message, $storedmessage); + + // Now let's try with failed status. + $mock->append(new Response( + status: 200, + )); + + $message = $manager->send( + recipientnumber: '+447123456789', + content: 'Hello, world!', + component: 'core', + messagetype: 'test', + recipientuserid: null, + async: false, + ); + + $this->assertEquals(message_status::GATEWAY_FAILED, $message->status); + $this->assertEquals($gw->id, $message->gatewayid); + } +} diff --git a/sms/gateway/modica/version.php b/sms/gateway/modica/version.php new file mode 100644 index 00000000000..cf7ba5e20db --- /dev/null +++ b/sms/gateway/modica/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version information for smsgateway_modica. + * + * @package smsgateway_modica + * @copyright 2025 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'smsgateway_modica'; +$plugin->version = 2025022500; +$plugin->requires = 2024100100; +$plugin->maturity = MATURITY_STABLE;