diff --git a/composer.json b/composer.json index 9674bce2fd..eff2e79e2d 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "utopia-php/image": "0.7.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.6.*", - "utopia-php/messaging": "0.13.*", + "utopia-php/messaging": "0.14.*", "utopia-php/migration": "0.6.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.7.1", diff --git a/composer.lock b/composer.lock index cd14e512a7..b3c114c81c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3cd37ea04612e04b9e76eb51c5da41dd", + "content-hash": "8a6a8d485f68d2ce144a7ee8bee424ac", "packages": [ { "name": "adhocore/jwt", @@ -1237,16 +1237,16 @@ }, { "name": "open-telemetry/api", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "351a30baa79699de3de3a814c8ccc7b52ccdfb1d" + "reference": "74b1a03263be8c5acb578f41da054b4bac3af4a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/351a30baa79699de3de3a814c8ccc7b52ccdfb1d", - "reference": "351a30baa79699de3de3a814c8ccc7b52ccdfb1d", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/74b1a03263be8c5acb578f41da054b4bac3af4a0", + "reference": "74b1a03263be8c5acb578f41da054b4bac3af4a0", "shasum": "" }, "require": { @@ -1303,7 +1303,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-08T23:50:34+00:00" + "time": "2025-01-20T23:35:16+00:00" }, { "name": "open-telemetry/context", @@ -1493,16 +1493,16 @@ }, { "name": "open-telemetry/sdk", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "9a1c3b866239dbff291e5cc555bb7793eab08127" + "reference": "96aeaee5b7cb8c0bc4af7ff4717b429f2d9f67e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/9a1c3b866239dbff291e5cc555bb7793eab08127", - "reference": "9a1c3b866239dbff291e5cc555bb7793eab08127", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/96aeaee5b7cb8c0bc4af7ff4717b429f2d9f67e1", + "reference": "96aeaee5b7cb8c0bc4af7ff4717b429f2d9f67e1", "shasum": "" }, "require": { @@ -1579,7 +1579,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-08T23:50:34+00:00" + "time": "2025-01-09T23:17:14+00:00" }, { "name": "open-telemetry/sem-conv", @@ -3878,16 +3878,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.13.0", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "0e3e57351fe4fe875ef3ab9a01a7fff5f022de90" + "reference": "4ba356a3aa382802727f7e13e0f0152bcc1fc535" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/0e3e57351fe4fe875ef3ab9a01a7fff5f022de90", - "reference": "0e3e57351fe4fe875ef3ab9a01a7fff5f022de90", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/4ba356a3aa382802727f7e13e0f0152bcc1fc535", + "reference": "4ba356a3aa382802727f7e13e0f0152bcc1fc535", "shasum": "" }, "require": { @@ -3923,9 +3923,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.13.0" + "source": "https://github.com/utopia-php/messaging/tree/0.14.1" }, - "time": "2024-12-05T08:36:07+00:00" + "time": "2025-01-28T06:14:28+00:00" }, { "name": "utopia-php/migration", @@ -5601,70 +5601,18 @@ }, "time": "2023-10-30T13:38:26+00:00" }, - { - "name": "phpbench/dom", - "version": "0.3.3", - "source": { - "type": "git", - "url": "https://github.com/phpbench/dom.git", - "reference": "786a96db538d0def931f5b19225233ec42ec7a72" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpbench/dom/zipball/786a96db538d0def931f5b19225233ec42ec7a72", - "reference": "786a96db538d0def931f5b19225233ec42ec7a72", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "php": "^7.3||^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.14", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.0||^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "PhpBench\\Dom\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Leech", - "email": "daniel@dantleech.com" - } - ], - "description": "DOM wrapper to simplify working with the PHP DOM implementation", - "support": { - "issues": "https://github.com/phpbench/dom/issues", - "source": "https://github.com/phpbench/dom/tree/0.3.3" - }, - "abandoned": true, - "time": "2023-03-06T23:46:57+00:00" - }, { "name": "phpbench/phpbench", - "version": "1.3.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "a3e1ef08d9d7736d43a7fbd444893d6a073c0ca0" + "reference": "4248817222514421cba466bfa7adc7d8932345d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/a3e1ef08d9d7736d43a7fbd444893d6a073c0ca0", - "reference": "a3e1ef08d9d7736d43a7fbd444893d6a073c0ca0", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/4248817222514421cba466bfa7adc7d8932345d4", + "reference": "4248817222514421cba466bfa7adc7d8932345d4", "shasum": "" }, "require": { @@ -5677,7 +5625,6 @@ "ext-tokenizer": "*", "php": "^8.1", "phpbench/container": "^2.2", - "phpbench/dom": "~0.3.3", "psr/log": "^1.1 || ^2.0 || ^3.0", "seld/jsonlint": "^1.1", "symfony/console": "^6.1 || ^7.0", @@ -5696,8 +5643,8 @@ "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.4", - "rector/rector": "^0.18.11 || ^1.0.0", + "phpunit/phpunit": "^10.4 || ^11.0", + "rector/rector": "^1.2", "symfony/error-handler": "^6.1 || ^7.0", "symfony/var-dumper": "^6.1 || ^7.0" }, @@ -5742,7 +5689,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.3.1" + "source": "https://github.com/phpbench/phpbench/tree/1.4.0" }, "funding": [ { @@ -5750,7 +5697,7 @@ "type": "github" } ], - "time": "2024-06-30T11:04:37+00:00" + "time": "2025-01-26T19:54:45+00:00" }, { "name": "phpdocumentor/reflection-common", diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 7837957c5d..aee60a2bb5 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -22,6 +22,8 @@ use Utopia\Messaging\Adapter\Push\APNS; use Utopia\Messaging\Adapter\Push as PushAdapter; use Utopia\Messaging\Adapter\Push\FCM; use Utopia\Messaging\Adapter\SMS as SMSAdapter; +use Utopia\Messaging\Adapter\SMS\Fast2SMS; +use Utopia\Messaging\Adapter\SMS\GEOSMS; use Utopia\Messaging\Adapter\SMS\Mock; use Utopia\Messaging\Adapter\SMS\Msg91; use Utopia\Messaging\Adapter\SMS\Telesign; @@ -46,6 +48,8 @@ class Messaging extends Action { private ?Local $localDevice = null; + private ?SMSAdapter $adapter = null; + public static function getName(): string { return 'messaging'; @@ -56,6 +60,9 @@ class Messaging extends Action */ public function __construct() { + + $this->adapter = $this->createInternalSMSAdapter(); + $this ->desc('Messaging worker') ->inject('message') @@ -381,100 +388,38 @@ class Messaging extends Action private function sendInternalSMSMessage(Document $message, Document $project, array $recipients, Log $log): void { - if (empty(System::getEnv('_APP_SMS_PROVIDER')) || empty(System::getEnv('_APP_SMS_FROM'))) { - throw new \Exception('Skipped SMS processing. Missing "_APP_SMS_PROVIDER" or "_APP_SMS_FROM" environment variables.'); + if ($this->adapter === null) { + Console::warning('Skipped SMS processing. SMS adapter is not set.'); + return; } if ($project->isEmpty()) { throw new \Exception('Project not set in payload'); } - Console::log('Project: ' . $project->getId()); - + Console::log('Processing project: ' . $project->getId()); $denyList = System::getEnv('_APP_SMS_PROJECTS_DENY_LIST', ''); $denyList = explode(',', $denyList); - if (\in_array($project->getId(), $denyList)) { Console::error('Project is in the deny list. Skipping...'); return; } - $smsDSN = new DSN(System::getEnv('_APP_SMS_PROVIDER')); - $host = $smsDSN->getHost(); - $password = $smsDSN->getPassword(); - $user = $smsDSN->getUser(); - - $log->addTag('type', $host); - - $from = System::getEnv('_APP_SMS_FROM'); - - $provider = new Document([ - '$id' => ID::unique(), - 'provider' => $host, - 'type' => MESSAGE_TYPE_SMS, - 'name' => 'Internal SMS', - 'enabled' => true, - 'credentials' => match ($host) { - 'twilio' => [ - 'accountSid' => $user, - 'authToken' => $password, - // Twilio Messaging Service SIDs always start with MG - // https://www.twilio.com/docs/messaging/services - 'messagingServiceSid' => \str_starts_with($from, 'MG') ? $from : null - ], - 'textmagic' => [ - 'username' => $user, - 'apiKey' => $password - ], - 'telesign' => [ - 'customerId' => $user, - 'apiKey' => $password - ], - 'msg91' => [ - 'senderId' => $user, - 'authKey' => $password, - 'templateId' => $smsDSN->getParam('templateId', $from), - ], - 'vonage' => [ - 'apiKey' => $user, - 'apiSecret' => $password - ], - default => null - }, - 'options' => match ($host) { - 'twilio' => [ - 'from' => \str_starts_with($from, 'MG') ? null : $from - ], - default => [ - 'from' => $from - ] - } - ]); - - $adapter = $this->getSmsAdapter($provider); - - $batches = \array_chunk( + $from = System::getEnv('_APP_SMS_FROM', ''); + $sms = new SMS( $recipients, - $adapter->getMaxMessagesPerRequest() + $message->getAttribute('data')['content'], + $from ); - batch(\array_map(function ($batch) use ($message, $provider, $adapter) { - return function () use ($batch, $message, $provider, $adapter) { - $message->setAttribute('to', $batch); - - $data = $this->buildSmsMessage($message, $provider); - - try { - $adapter->send($data); - } catch (\Throwable $th) { - throw new \Exception('Failed sending to targets with error: ' . $th->getMessage()); - } - }; - }, $batches)); + try { + $result = $this->adapter->send($sms); + } catch (\Throwable $th) { + throw new \Exception('Failed sending to targets with error: ' . $th->getMessage()); + } } - private function getSmsAdapter(Document $provider): ?SMSAdapter { $credentials = $provider->getAttribute('credentials'); @@ -504,6 +449,12 @@ class Messaging extends Action $credentials['apiKey'] ?? '', $credentials['apiSecret'] ?? '' ), + 'fast2sms' => new Fast2SMS( + $credentials['apiKey'] ?? '', + $credentials['senderId'] ?? '', + $credentials['messageId'] ?? '', + $credentials['useDLT'] ?? true + ), default => null }; } @@ -720,4 +671,127 @@ class Messaging extends Action return $this->localDevice; } + + private function createInternalSMSAdapter(): ?SMSAdapter + { + if (empty(System::getEnv('_APP_SMS_PROVIDER')) || empty(System::getEnv('_APP_SMS_FROM'))) { + Console::warning('Skipped SMS processing. Missing "_APP_SMS_PROVIDER" or "_APP_SMS_FROM" environment variables.'); + return null; + } + + $providers = System::getEnv('_APP_SMS_PROVIDER', ''); + + $dsns = []; + if (!empty($providers)) { + $providers = explode(',', $providers); + foreach ($providers as $provider) { + $dsns[] = new DSN($provider); + } + } + + if (count($dsns) === 1) { + $provider = $this->createProviderFromDSN($dsns[0]); + $adapter = $this->getSmsAdapter($provider); + return $adapter; + } + + $defaultDSN = null; + $localDSNs = []; + + /** @var DSN $dsn */ + foreach ($dsns as $dsn) { + if ($dsn->getParam('local', '') === 'default') { + $defaultDSN = $dsn; + } else { + $localDSNs[] = $dsn; + } + } + + if ($defaultDSN === null) { + throw new \Exception('No default SMS provider found'); + } + + $defaultProvider = $this->createProviderFromDSN($defaultDSN); + $adapter = $this->getSmsAdapter($defaultProvider); + $geosms = new GEOSMS($adapter); + + /** @var DSN $localDSN */ + foreach ($localDSNs as $localDSN) { + try { + $provider = $this->createProviderFromDSN($localDSN); + $adapter = $this->getSmsAdapter($provider); + } catch (\Exception) { + Console::warning('Unable to create adapter: ' . $localDSN->getHost()); + continue; + } + + $callingCode = $localDSN->getParam('local', ''); + if (empty($callingCode)) { + Console::warning('Unable to register adapter: ' . $localDSN->getHost() . '. Missing `local` parameter.'); + continue; + } + + $geosms->setLocal($callingCode, $adapter); + } + return $geosms; + } + + private function createProviderFromDSN(DSN $dsn): Document + { + $host = $dsn->getHost(); + $password = $dsn->getPassword(); + $user = $dsn->getUser(); + $from = System::getEnv('_APP_SMS_FROM'); + + $provider = new Document([ + '$id' => ID::unique(), + 'provider' => $host, + 'type' => MESSAGE_TYPE_SMS, + 'name' => 'Internal SMS', + 'enabled' => true, + 'credentials' => match ($host) { + 'twilio' => [ + 'accountSid' => $user, + 'authToken' => $password, + // Twilio Messaging Service SIDs always start with MG + // https://www.twilio.com/docs/messaging/services + 'messagingServiceSid' => \str_starts_with($from, 'MG') ? $from : null + ], + 'textmagic' => [ + 'username' => $user, + 'apiKey' => $password + ], + 'telesign' => [ + 'customerId' => $user, + 'apiKey' => $password + ], + 'msg91' => [ + 'senderId' => $user, + 'authKey' => $password, + 'templateId' => $dsn->getParam('templateId', $from), + ], + 'vonage' => [ + 'apiKey' => $user, + 'apiSecret' => $password + ], + 'fast2sms' => [ + 'senderId' => $user, + 'apiKey' => $password, + 'messageId' => $dsn->getParam('messageId'), + 'useDLT' => $dsn->getParam('useDLT'), + ], + default => null + }, + 'options' => match ($host) { + 'twilio' => [ + 'from' => \str_starts_with($from, 'MG') ? null : $from + ], + default => [ + 'from' => $from + ] + } + ]); + + return $provider; + } }