diff --git a/.github/workflows/analyse.yml b/.github/workflows/analyse.yml
new file mode 100644
index 00000000..72ed14c1
--- /dev/null
+++ b/.github/workflows/analyse.yml
@@ -0,0 +1,20 @@
+name: "Static Analysis"
+
+on: [pull_request]
+jobs:
+ analyse:
+ name: Analyse
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 2
+
+ - run: git checkout HEAD^2
+
+ - name: Run Static Analysis
+ run: |
+ docker run --rm -v $PWD:/app composer sh -c \
+ "composer install --profile --ignore-platform-reqs && composer analyse"
diff --git a/.github/workflows/linter.yml b/.github/workflows/lint.yml
similarity index 99%
rename from .github/workflows/linter.yml
rename to .github/workflows/lint.yml
index 4ee16844..ffb8a097 100644
--- a/.github/workflows/linter.yml
+++ b/.github/workflows/lint.yml
@@ -11,7 +11,9 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 2
+
- run: git checkout HEAD^2
+
- name: Run Linter
run: |
docker run --rm -v $PWD:/app composer sh -c \
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..47086952
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,48 @@
+name: "Tests"
+
+on: [pull_request]
+jobs:
+ tests:
+ name: Unit & E2E
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 2
+ - run: git checkout HEAD^2
+ - name: Run Tests
+ env:
+ MAILGUN_API_KEY: ${{ secrets.MAILGUN_API_KEY }}
+ MAILGUN_DOMAIN: ${{ secrets.MAILGUN_DOMAIN }}
+ SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
+ FCM_SERVER_KEY: ${{ secrets.FCM_SERVER_KEY }}
+ FCM_SERVER_TO: ${{ secrets.FCM_SERVER_TO }}
+ TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
+ TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
+ TWILIO_TO: ${{ secrets.TWILIO_TO }}
+ TWILIO_FROM: ${{ secrets.TWILIO_FROM }}
+ TELNYX_API_KEY: ${{ secrets.TELNYX_API_KEY }}
+ TELNYX_PUBLIC_KEY: ${{ secrets.TELNYX_PUBLIC_KEY }}
+ APNS_AUTHKEY_8KVVCLA3HL: ${{ secrets.APNS_AUTHKEY_8KVVCLA3HL }}
+ APNS_AUTH_ID: ${{ secrets.APNS_AUTH_ID }}
+ APNS_TEAM_ID: ${{ secrets.APNS_TEAM_ID }}
+ APNS_BUNDLE_ID: ${{ secrets.APNS_BUNDLE_ID }}
+ APNS_TO: ${{ secrets.APNS_TO }}
+ MSG_91_SENDER_ID: ${{ secrets.MSG_91_SENDER_ID }}
+ MSG_91_AUTH_KEY: ${{ secrets.MSG_91_AUTH_KEY }}
+ MSG_91_TO: ${{ secrets.MSG_91_TO }}
+ MSG_91_FROM: ${{ secrets.MSG_91_FROM }}
+ TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
+ TEST_FROM_EMAIL: ${{ secrets.TEST_FROM_EMAIL }}
+ VONAGE_API_KEY: ${{ secrets.VONAGE_API_KEY }}
+ VONAGE_API_SECRET: ${{ secrets.VONAGE_API_SECRET }}
+ VONAGE_TO: ${{ secrets.VONAGE_TO }}
+ VONAGE_FROM: ${{ secrets.VONAGE_FROM }}
+ DISCORD_WEBHOOK_ID: ${{ secrets.DISCORD_WEBHOOK_ID }}
+ DISCORD_WEBHOOK_TOKEN: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
+ run: |
+ docker compose up -d --build
+ sleep 5
+ docker compose exec tests vendor/bin/phpunit
\ No newline at end of file
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index 901e25bf..00000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-name: "Tests"
-
-on: [pull_request]
-jobs:
- tests:
- name: Unit & E2E
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v2
- with:
- fetch-depth: 2
- - run: git checkout HEAD^2
- - name: Run Tests
- run: |
- docker compose up -d --build
- sleep 5
- docker compose exec tests vendor/bin/phpunit
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 24265e89..3640a16f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,97 @@
.idea
vendor
+Makefile
+.envrc
+.env
+*.p8
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### PHPUnit ###
+# Covers PHPUnit
+# Reference: https://phpunit.de/
+
+# Generated files
.phpunit.result.cache
+.phpunit.cache
+
+# PHPUnit
+/app/phpunit.xml
+/phpunit.xml
+
+# Build data
+/build/
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/macos,linux,windows,phpunit
\ No newline at end of file
diff --git a/README.md b/README.md
index 3baa3047..363d928e 100644
--- a/README.md
+++ b/README.md
@@ -15,14 +15,14 @@ Install using composer:
composer require utopia-php/messaging
```
-## Email
+## Email
```php
send($message);
send($message);
send($message);
- [x] [TextMagic](https://www.textmagic.com/)
- [x] [Msg91](https://msg91.com/)
- [x] [Vonage](https://www.vonage.com/)
-- [ ] [Plivo](https://www.plivo.com/)
-- [ ] [Infobip](https://www.infobip.com/)
-- [ ] [Clickatell](https://www.clickatell.com/)
+- [x] [Plivo](https://www.plivo.com/)
+- [x] [Infobip](https://www.infobip.com/)
+- [x] [Clickatell](https://www.clickatell.com/)
- [ ] [AfricasTalking](https://africastalking.com/)
-- [ ] [Sinch](https://www.sinch.com/)
-- [ ] [Sms77](https://www.sms77.io/)
+- [x] [Sinch](https://www.sinch.com/)
+- [x] [Seven](https://www.seven.io/)
- [ ] [SmsGlobal](https://www.smsglobal.com/)
### Push
@@ -135,13 +135,6 @@ To run static code analysis, use the following Psalm command:
composer lint
```
-## Authors
-
-**Jake Barnby**
-
-+ [https://github.com/abnegate](https://github.com/abnegate)
-+ [https://nz.linkedin.com/in/jakebarnby](https://nz.linkedin.com/in/jakebarnby)
-
## Copyright and license
The MIT License (MIT) [http://www.opensource.org/licenses/mit-license.php](http://www.opensource.org/licenses/mit-license.php)
diff --git a/composer.json b/composer.json
index e94220b3..f76d03af 100644
--- a/composer.json
+++ b/composer.json
@@ -6,16 +6,11 @@
"license": "MIT",
"minimum-stability": "stable",
"scripts": {
- "test": "vendor/bin/phpunit",
- "lint": "vendor/bin/phpcs",
- "format": "vendor/bin/phpcbf"
+ "test": "./vendor/bin/phpunit",
+ "lint": "./vendor/bin/pint --test",
+ "format": "./vendor/bin/pint",
+ "analyse": "./vendor/bin/phpstan analyse --memory-limit=2G --level=6 src tests"
},
- "authors": [
- {
- "name": "Jake Barnby",
- "email": "jake@appwrite.io"
- }
- ],
"autoload": {
"psr-4": {
"Utopia\\Messaging\\": "src/Utopia/Messaging"
@@ -23,8 +18,7 @@
},
"autoload-dev": {
"psr-4": {
- "Tests\\E2E\\": "tests/e2e",
- "Tests\\Unit\\": "tests/unit"
+ "Utopia\\Tests\\": "tests/Messaging"
}
},
"require": {
@@ -32,9 +26,11 @@
"ext-curl": "*"
},
"require-dev": {
- "phpunit/phpunit": "9.5.*",
- "phpmailer/phpmailer": "6.6.*",
- "squizlabs/php_codesniffer": "^3.6"
+ "ext-openssl": "*",
+ "phpunit/phpunit": "9.6.10",
+ "phpmailer/phpmailer": "6.8.*",
+ "laravel/pint": "1.13.*",
+ "phpstan/phpstan": "1.10.*"
},
"config": {
"platform": {
diff --git a/composer.lock b/composer.lock
index 88f16b81..df72e795 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": "e33a07c2dd4a4d32669369f3e04a7a21",
+ "content-hash": "1acdc1e8f88c00fae7fc271daa90a725",
"packages": [],
"packages-dev": [
{
@@ -77,6 +77,72 @@
],
"time": "2022-03-03T08:28:38+00:00"
},
+ {
+ "name": "laravel/pint",
+ "version": "v1.13.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/pint.git",
+ "reference": "3e3d2ab01c7d8b484c18e6100ecf53639c744fa7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/3e3d2ab01c7d8b484c18e6100ecf53639c744fa7",
+ "reference": "3e3d2ab01c7d8b484c18e6100ecf53639c744fa7",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "ext-tokenizer": "*",
+ "ext-xml": "*",
+ "php": "^8.1.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.38.0",
+ "illuminate/view": "^10.30.1",
+ "laravel-zero/framework": "^10.3.0",
+ "mockery/mockery": "^1.6.6",
+ "nunomaduro/larastan": "^2.6.4",
+ "nunomaduro/termwind": "^1.15.1",
+ "pestphp/pest": "^2.24.2"
+ },
+ "bin": [
+ "builds/pint"
+ ],
+ "type": "project",
+ "autoload": {
+ "psr-4": {
+ "App\\": "app/",
+ "Database\\Seeders\\": "database/seeders/",
+ "Database\\Factories\\": "database/factories/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "An opinionated code formatter for PHP.",
+ "homepage": "https://laravel.com",
+ "keywords": [
+ "format",
+ "formatter",
+ "lint",
+ "linter",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/pint/issues",
+ "source": "https://github.com/laravel/pint"
+ },
+ "time": "2023-11-07T17:59:57+00:00"
+ },
{
"name": "myclabs/deep-copy",
"version": "1.11.0",
@@ -305,16 +371,16 @@
},
{
"name": "phpmailer/phpmailer",
- "version": "v6.6.4",
+ "version": "v6.8.0",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
- "reference": "a94fdebaea6bd17f51be0c2373ab80d3d681269b"
+ "reference": "df16b615e371d81fb79e506277faea67a1be18f1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/a94fdebaea6bd17f51be0c2373ab80d3d681269b",
- "reference": "a94fdebaea6bd17f51be0c2373ab80d3d681269b",
+ "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/df16b615e371d81fb79e506277faea67a1be18f1",
+ "reference": "df16b615e371d81fb79e506277faea67a1be18f1",
"shasum": ""
},
"require": {
@@ -324,22 +390,24 @@
"php": ">=5.5.0"
},
"require-dev": {
- "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
- "doctrine/annotations": "^1.2",
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2",
+ "doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
- "squizlabs/php_codesniffer": "^3.6.2",
- "yoast/phpunit-polyfills": "^1.0.0"
+ "squizlabs/php_codesniffer": "^3.7.1",
+ "yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+ "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+ "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
- "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication",
- "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
+ "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+ "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"type": "library",
"autoload": {
@@ -371,7 +439,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
- "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.4"
+ "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.0"
},
"funding": [
{
@@ -379,7 +447,69 @@
"type": "github"
}
],
- "time": "2022-08-22T09:22:00+00:00"
+ "time": "2023-03-06T14:43:22+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "1.10.44",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan.git",
+ "reference": "bf84367c53a23f759513985c54ffe0d0c249825b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/bf84367c53a23f759513985c54ffe0d0c249825b",
+ "reference": "bf84367c53a23f759513985c54ffe0d0c249825b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-11-21T16:30:46+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -701,20 +831,20 @@
},
{
"name": "phpunit/phpunit",
- "version": "9.5.25",
+ "version": "9.6.10",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d"
+ "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d",
- "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a6d351645c3fe5a30f5e86be6577d946af65a328",
+ "reference": "a6d351645c3fe5a30f5e86be6577d946af65a328",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.3.1",
+ "doctrine/instantiator": "^1.3.1 || ^2",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
@@ -743,8 +873,8 @@
"sebastian/version": "^3.0.2"
},
"suggest": {
- "ext-soap": "*",
- "ext-xdebug": "*"
+ "ext-soap": "To be able to generate mocks based on WSDL files",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
},
"bin": [
"phpunit"
@@ -752,7 +882,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "9.5-dev"
+ "dev-master": "9.6-dev"
}
},
"autoload": {
@@ -783,7 +913,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.25"
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.10"
},
"funding": [
{
@@ -799,7 +930,7 @@
"type": "tidelift"
}
],
- "time": "2022-09-25T03:44:45+00:00"
+ "time": "2023-07-10T04:04:23+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -1765,62 +1896,6 @@
],
"time": "2020-09-28T06:39:44+00:00"
},
- {
- "name": "squizlabs/php_codesniffer",
- "version": "3.7.1",
- "source": {
- "type": "git",
- "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
- "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
- "shasum": ""
- },
- "require": {
- "ext-simplexml": "*",
- "ext-tokenizer": "*",
- "ext-xmlwriter": "*",
- "php": ">=5.4.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
- },
- "bin": [
- "bin/phpcs",
- "bin/phpcbf"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.x-dev"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Greg Sherwood",
- "role": "lead"
- }
- ],
- "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
- "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
- "keywords": [
- "phpcs",
- "standards"
- ],
- "support": {
- "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
- "source": "https://github.com/squizlabs/PHP_CodeSniffer",
- "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
- },
- "time": "2022-06-18T07:21:10+00:00"
- },
{
"name": "theseer/tokenizer",
"version": "1.2.1",
@@ -1881,9 +1956,11 @@
"php": ">=8.0.0",
"ext-curl": "*"
},
- "platform-dev": [],
+ "platform-dev": {
+ "ext-openssl": "*"
+ },
"platform-overrides": {
"php": "8.0"
},
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 96054433..6cb8400a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,13 +2,42 @@ version: '3.9'
services:
tests:
+ environment:
+ - MAILGUN_API_KEY
+ - MAILGUN_DOMAIN
+ - SENDGRID_API_KEY
+ - FCM_SERVER_KEY
+ - FCM_SERVER_TO
+ - TWILIO_ACCOUNT_SID
+ - TWILIO_AUTH_TOKEN
+ - TWILIO_TO
+ - TWILIO_FROM
+ - TELNYX_API_KEY
+ - TELNYX_PUBLIC_KEY
+ - APNS_AUTHKEY_8KVVCLA3HL
+ - APNS_AUTH_ID
+ - APNS_TEAM_ID
+ - APNS_BUNDLE_ID
+ - APNS_TO
+ - MSG_91_SENDER_ID
+ - MSG_91_AUTH_KEY
+ - MSG_91_TO
+ - MSG_91_FROM
+ - TEST_EMAIL
+ - TEST_FROM_EMAIL
+ - VONAGE_API_KEY
+ - VONAGE_API_SECRET
+ - VONAGE_TO
+ - VONAGE_FROM
+ - DISCORD_WEBHOOK_ID
+ - DISCORD_WEBHOOK_TOKEN
build:
context: .
volumes:
- ./src:/usr/local/src/src
- ./tests:/usr/local/src/tests
- ./phpunit.xml:/usr/local/src/phpunit.xml
-
+
maildev:
image: appwrite/mailcatcher:1.0.0
ports:
diff --git a/docs/add-new-adapter.md b/docs/add-new-adapter.md
index a720bbc0..70d3d38d 100644
--- a/docs/add-new-adapter.md
+++ b/docs/add-new-adapter.md
@@ -81,10 +81,10 @@ Putting it all together, the `SendGrid` adapter should look like this:
```php
high_
+
+## SMS Target
+
+_You can add your country with a full pricing table._
+
+- [United States](#united-states)
+- [India](#india)
+- [Israel](#israel)
+
+## United States
+
+_In most providers when you're sending SMS to US numbers you must own a US phone number to provide it in the `from` field. Other might add random phone number they have_
+
+| Provider | Unit Price | 10K |
+|------------|------------|-------|
+| Plivo | 0.0050 | 50 $ |
+| Telesign | 0.0062 | 62 $ |
+| Msg91 | 0.0067 | 67 $ |
+| Vonage | 0.0067 | 67 $ |
+| Infobip | 0.0070 | 70 $ |
+| Clickatell | 0.0075 | 75 $ |
+| Twilio | 0.0079 | 79 $ |
+| Sinch | 0.0120 | 120 $ |
+| TextMagic | 0.0400 | 400 $ |
+| Telnyx | 0.0440 | 440 $ |
+| Seven | 0.0810 | 810 $ |
+
+## India
+
+| Provider | Unit Price | 10K |
+|------------|------------|-------|
+| Msg91 | 0.0030 | 30 $ |
+| Clickatell | 0.0426 | 426 $ |
+| Vonage | 0.0449 | 449 $ |
+| Telesign | 0.0485 | 485 $ |
+| Telnyx | 0.0490 | 490 $ |
+| Twilio | 0.0490 | 490 $ |
+| Plivo | 0.0560 | 560 $ |
+| Seven | 0.0810 | 810 $ |
+| Infobip | n/a | |
+| Sinch | n/a | |
+| TextMagic | n/a | |
+
+## Israel
+
+| Provider | Unit Price | 10K |
+|------------|------------|----------|
+| Telesign | 0.0768 | 768 $ |
+| Seven | 0.0810 | 810 $ |
+| Msg91 | 0.0845 | 845 $ |
+| Plivo | 0.101 | 1010 $ |
+| Vonage | 0.1019 | 1019 $ |
+| Infobip | 0.106 | 1060 $ |
+| Telnyx | 0.1100 | 1100 $ |
+| Twilio | 0.112 | 1120 $ |
+| Clickatell | 0.13144 | 1314.4 $ |
+| Sinch | n/a | |
+| TextMagic | n/a | |
diff --git a/phpunit.xml b/phpunit.xml
index 8db6586a..bb763030 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -9,11 +9,8 @@
stopOnFailure="false"
>
-
- ./tests/unit
-
-
- ./tests/e2e/
+
+ ./tests/
\ No newline at end of file
diff --git a/src/Utopia/Messaging/Adapter.php b/src/Utopia/Messaging/Adapter.php
index 83422709..6fdfc285 100644
--- a/src/Utopia/Messaging/Adapter.php
+++ b/src/Utopia/Messaging/Adapter.php
@@ -6,74 +6,81 @@ abstract class Adapter
{
/**
* Get the name of the adapter.
- *
- * @return string
*/
abstract public function getName(): string;
/**
* Get the type of the adapter.
- *
- * @return string
*/
abstract public function getType(): string;
/**
* Get the type of the message the adapter can send.
- *
- * @return string
*/
abstract public function getMessageType(): string;
/**
* Get the maximum number of messages that can be sent in a single request.
- *
- * @return int
*/
abstract public function getMaxMessagesPerRequest(): int;
/**
* Send a message.
*
- * @param Message $message The message to send.
+ * @param Message $message The message to send.
* @return string The response body.
+ *
+ * @throws \Exception
*/
- abstract public function send(Message $message): string;
+ public function send(Message $message): string
+ {
+ if (! \is_a($message, $this->getMessageType())) {
+ throw new \Exception('Invalid message type.');
+ }
+ if (\method_exists($message, 'getTo') && \count($message->getTo()) > $this->getMaxMessagesPerRequest()) {
+ throw new \Exception("{$this->getName()} can only send {$this->getMaxMessagesPerRequest()} messages per request.");
+ }
+ if (! \method_exists($this, 'process')) {
+ throw new \Exception('Adapter does not implement process method.');
+ }
+
+ return $this->process($message);
+ }
/**
* Send an HTTP request.
*
- * @param string $method The HTTP method to use.
- * @param string $url The URL to send the request to.
- * @param array $headers An array of headers to send with the request.
- * @param string|null $body The body of the request.
+ * @param string $method The HTTP method to use.
+ * @param string $url The URL to send the request to.
+ * @param array $headers An array of headers to send with the request.
+ * @param string|null $body The body of the request.
* @return string The response body.
+ *
* @throws \Exception If the request fails.
*/
protected function request(
string $method,
string $url,
array $headers = [],
- mixed $body = null,
+ string $body = null,
): string {
- $headers[] = 'Content-length: ' . \strlen($body);
-
$ch = \curl_init();
+ if (! \is_null($body)) {
+ $headers[] = 'Content-Length: '.\strlen($body);
+ \curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
+ }
+
\curl_setopt($ch, CURLOPT_URL, $url);
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
\curl_setopt($ch, CURLOPT_USERAGENT, "Appwrite {$this->getName()} Message Sender");
- if (!is_null($body)) {
- \curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
- }
-
$response = \curl_exec($ch);
if (\curl_errno($ch)) {
- throw new \Exception('Error:' . \curl_error($ch));
+ throw new \Exception('Error: '.\curl_error($ch));
}
if (\curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
throw new \Exception($response);
diff --git a/src/Utopia/Messaging/Adapter/Chat/Discord.php b/src/Utopia/Messaging/Adapter/Chat/Discord.php
new file mode 100644
index 00000000..7b0deea6
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/Chat/Discord.php
@@ -0,0 +1,82 @@
+getWait())) {
+ $query['wait'] = $message->getWait();
+ }
+ if (! \is_null($message->getThreadId())) {
+ $query['thread_id'] = $message->getThreadId();
+ }
+
+ $queryString = '';
+ foreach ($query as $key => $value) {
+ if (empty($queryString)) {
+ $queryString .= '?';
+ }
+ $queryString .= $key.'='.$value;
+ }
+
+ return $this->request(
+ method: 'POST',
+ url: "https://discord.com/api/webhooks/{$this->webhookId}/{$this->webhookToken}{$queryString}",
+ headers: [
+ 'Content-Type: application/json',
+ ],
+ body: \json_encode([
+ 'content' => $message->getContent(),
+ 'username' => $message->getUsername(),
+ 'avatar_url' => $message->getAvatarUrl(),
+ 'tts' => $message->getTTS(),
+ 'embeds' => $message->getEmbeds(),
+ 'allowed_mentions' => $message->getAllowedMentions(),
+ 'components' => $message->getComponents(),
+ 'attachments' => $message->getAttachments(),
+ 'flags' => $message->getFlags(),
+ 'thread_name' => $message->getThreadName(),
+ ]),
+ );
+ }
+}
diff --git a/src/Utopia/Messaging/Adapter/Email.php b/src/Utopia/Messaging/Adapter/Email.php
new file mode 100644
index 00000000..9b395a0e
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/Email.php
@@ -0,0 +1,28 @@
+request(
+ $usDomain = 'api.mailgun.net';
+ $euDomain = 'api.eu.mailgun.net';
+
+ $domain = $this->isEU ? $euDomain : $usDomain;
+
+ $response = $this->request(
method: 'POST',
- url: "https://api.mailgun.net/v3/{$this->domain}/messages",
+ url: "https://$domain/v3/{$this->domain}/messages",
headers: [
- 'Authorization: Basic ' . base64_encode('api:' . $this->apiKey)
+ 'Authorization: Basic '.base64_encode('api:'.$this->apiKey),
],
- body: [
- 'from' => $message->getFrom(),
+ body: \http_build_query([
'to' => \implode(',', $message->getTo()),
+ 'from' => $message->getFrom(),
'subject' => $message->getSubject(),
'text' => $message->isHtml() ? null : $message->getContent(),
'html' => $message->isHtml() ? $message->getContent() : null,
- ],
+ ]),
);
+
+ return $response;
}
}
diff --git a/src/Utopia/Messaging/Adapters/Email/Mock.php b/src/Utopia/Messaging/Adapter/Email/Mock.php
similarity index 85%
rename from src/Utopia/Messaging/Adapters/Email/Mock.php
rename to src/Utopia/Messaging/Adapter/Email/Mock.php
index c13a43fa..851a4d20 100644
--- a/src/Utopia/Messaging/Adapters/Email/Mock.php
+++ b/src/Utopia/Messaging/Adapter/Email/Mock.php
@@ -1,11 +1,11 @@
SMTPAuth = false;
$mail->Username = '';
$mail->Password = '';
- $mail->SMTPSecure = false;
+ $mail->SMTPSecure = '';
$mail->SMTPAutoTLS = false;
$mail->CharSet = 'UTF-8';
$mail->Subject = $message->getSubject();
@@ -49,10 +50,10 @@ protected function process(Email $message): string
$mail->addAddress($to);
}
- if (!$mail->send()) {
+ if (! $mail->send()) {
throw new \Exception($mail->ErrorInfo);
}
- return true;
+ return \json_encode($message);
}
}
diff --git a/src/Utopia/Messaging/Adapters/Email/Sendgrid.php b/src/Utopia/Messaging/Adapter/Email/Sendgrid.php
similarity index 68%
rename from src/Utopia/Messaging/Adapters/Email/Sendgrid.php
rename to src/Utopia/Messaging/Adapter/Email/Sendgrid.php
index dc0ec8e3..27be13bc 100644
--- a/src/Utopia/Messaging/Adapters/Email/Sendgrid.php
+++ b/src/Utopia/Messaging/Adapter/Email/Sendgrid.php
@@ -1,41 +1,56 @@
request(
method: 'POST',
url: 'https://api.sendgrid.com/v3/mail/send',
headers: [
- 'Authorization: Bearer ' . $this->apiKey,
+ 'Authorization: Bearer '.$this->apiKey,
'Content-Type: application/json',
],
body: \json_encode([
'personalizations' => [
[
'to' => \array_map(
- fn($to) => ['email' => $to],
+ fn ($to) => ['email' => $to],
$message->getTo()
),
'subject' => $message->getSubject(),
diff --git a/src/Utopia/Messaging/Adapter/Push.php b/src/Utopia/Messaging/Adapter/Push.php
new file mode 100644
index 00000000..d1c3cbe4
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/Push.php
@@ -0,0 +1,28 @@
+ [
+ 'alert' => [
+ 'title' => $message->getTitle(),
+ 'body' => $message->getBody(),
+ ],
+ 'badge' => $message->getBadge(),
+ 'sound' => $message->getSound(),
+ 'data' => $message->getData(),
+ ],
+ ];
+
+ return \json_encode($this->notify($message->getTo(), $payload));
+ }
+
+ /**
+ * @param array $to
+ * @param array $payload
+ * @return array
+ *
+ * @throws Exception
+ */
+ private function notify(array $to, array $payload): array
+ {
+ $headers = [
+ 'authorization: bearer '.$this->generateJwt(),
+ 'apns-topic: '.$this->bundleId,
+ 'apns-push-type: alert',
+ ];
+
+ $sh = \curl_share_init();
+
+ \curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
+
+ $ch = \curl_init();
+
+ \curl_setopt($ch, CURLOPT_SHARE, $sh);
+ \curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
+
+ \curl_setopt_array($ch, [
+ CURLOPT_PORT => 443,
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => \json_encode($payload),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 30,
+ CURLOPT_HEADER => true,
+ ]);
+
+ $response = '';
+ $endpoint = 'https://api.push.apple.com';
+
+ if ($this->sandbox) {
+ $endpoint = 'https://api.development.push.apple.com';
+ }
+
+ $mh = \curl_multi_init();
+ $handles = [];
+
+ // Create a handle for each request
+ foreach ($to as $token) {
+ \curl_setopt($ch, CURLOPT_URL, $endpoint.'/3/device/'.$token);
+
+ $handle = \curl_copy_handle($ch);
+ \curl_multi_add_handle($mh, $handle);
+
+ $handles[] = $handle;
+ }
+
+ $active = null;
+ $status = CURLM_OK;
+
+ // Execute the handles
+ while ($active && $status == CURLM_OK) {
+ $status = \curl_multi_exec($mh, $active);
+ }
+
+ // Check each handles result
+ $responses = [];
+ foreach ($handles as $ch) {
+ $urlInfo = \curl_getinfo($ch);
+ $device = basename($urlInfo['url']); // Extracts deviceToken from the URL
+
+ if (! \curl_errno($ch)) {
+ $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $responses[] = [
+ 'device' => $device,
+ 'status' => 'success',
+ 'statusCode' => $statusCode,
+ ];
+ } else {
+ $responses[$device] = [
+ 'status' => 'error',
+ 'error' => \curl_error($ch),
+ ];
+ }
+
+ \curl_multi_remove_handle($mh, $ch);
+ \curl_close($ch);
+ }
+
+ \curl_multi_close($mh);
+ \curl_share_close($sh);
+
+ return $responses;
+ }
+
+ /**
+ * Generate JWT.
+ *
+ *
+ * @throws Exception
+ */
+ private function generateJwt(): string
+ {
+ $header = json_encode(['alg' => 'ES256', 'kid' => $this->authKeyId]);
+ $claims = json_encode([
+ 'iss' => $this->teamId,
+ 'iat' => time(),
+ ]);
+
+ // Replaces URL sensitive characters that could be the result of base64 encoding.
+ // Replace to _ to avoid any special handling.
+ $base64UrlHeader = \str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
+ $base64UrlClaims = \str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($claims));
+
+ if (! $this->authKey) {
+ throw new \Exception('Invalid private key');
+ }
+
+ $signature = '';
+ $success = \openssl_sign("$base64UrlHeader.$base64UrlClaims", $signature, $this->authKey, OPENSSL_ALGO_SHA256);
+
+ if (! $success) {
+ throw new \Exception('Failed to sign JWT');
+ }
+
+ $base64UrlSignature = \str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));
+
+ return "$base64UrlHeader.$base64UrlClaims.$base64UrlSignature";
+ }
+}
diff --git a/src/Utopia/Messaging/Adapters/Push/FCM.php b/src/Utopia/Messaging/Adapter/Push/FCM.php
similarity index 80%
rename from src/Utopia/Messaging/Adapters/Push/FCM.php
rename to src/Utopia/Messaging/Adapter/Push/FCM.php
index 4205dc8f..4625b905 100644
--- a/src/Utopia/Messaging/Adapters/Push/FCM.php
+++ b/src/Utopia/Messaging/Adapter/Push/FCM.php
@@ -1,32 +1,39 @@
serverKey}",
],
body: \json_encode([
diff --git a/src/Utopia/Messaging/Adapter/SMS.php b/src/Utopia/Messaging/Adapter/SMS.php
new file mode 100644
index 00000000..bceb0506
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/SMS.php
@@ -0,0 +1,28 @@
+request(
+ method: 'POST',
+ url: 'https://platform.clickatell.com/messages',
+ headers: [
+ 'content-type: application/json',
+ 'Authorization: '.$this->apiKey,
+ ],
+ body: \json_encode([
+ 'content' => $message->getContent(),
+ 'from' => $this->from ?? $message->getFrom(),
+ 'to' => $message->getTo(),
+ ]),
+ );
+ }
+}
diff --git a/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php b/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php
new file mode 100644
index 00000000..b3f66429
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/SMS/GEOSMS.php
@@ -0,0 +1,120 @@
+
+ */
+ protected array $localAdapters = [];
+
+ public function __construct(SMSAdapter $defaultAdapter)
+ {
+ $this->defaultAdapter = $defaultAdapter;
+ }
+
+ public function getName(): string
+ {
+ return 'GEOSMS';
+ }
+
+ public function getMaxMessagesPerRequest(): int
+ {
+ return PHP_INT_MAX;
+ }
+
+ public function setLocal(string $callingCode, SMSAdapter $adapter): self
+ {
+ $this->localAdapters[$callingCode] = $adapter;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ protected function filterCallingCodesByAdapter(SMSAdapter $adapter): array
+ {
+ $result = [];
+
+ foreach ($this->localAdapters as $callingCode => $localAdapter) {
+ if ($localAdapter === $adapter) {
+ $result[] = $callingCode;
+ }
+ }
+
+ return $result;
+ }
+
+ protected function process(SMS $message): string
+ {
+ $results = [];
+ $recipients = $message->getTo();
+
+ do {
+ [$nextRecipients, $nextAdapter] = $this->getNextRecipientsAndAdapter($recipients);
+
+ try {
+ $results[$nextAdapter->getName()] = json_decode($nextAdapter->send(
+ new SMS(
+ to: $nextRecipients,
+ content: $message->getContent(),
+ from: $message->getFrom(),
+ attachments: $message->getAttachments()
+ )
+ ));
+ } catch (\Exception $e) {
+ $results[$nextAdapter->getName()] = [
+ 'type' => 'error',
+ 'message' => $e->getMessage(),
+ ];
+ }
+
+ $recipients = \array_diff($recipients, $nextRecipients);
+ } while (count($recipients) > 0);
+
+ return \json_encode($results);
+ }
+
+ /**
+ * @param array $recipients
+ * @return array|SMSAdapter>
+ */
+ protected function getNextRecipientsAndAdapter(array $recipients): array
+ {
+ $nextRecipients = [];
+ $nextAdapter = null;
+
+ foreach ($recipients as $recipient) {
+ $adapter = $this->getAdapterByPhoneNumber($recipient);
+
+ if ($nextAdapter === null || $adapter === $nextAdapter) {
+ $nextAdapter = $adapter;
+ $nextRecipients[] = $recipient;
+ }
+ }
+
+ return [$nextRecipients, $nextAdapter];
+ }
+
+ protected function getAdapterByPhoneNumber(?string $phoneNumber): SMSAdapter
+ {
+ $callingCode = CallingCode::fromPhoneNumber($phoneNumber);
+ if (empty($callingCode)) {
+ return $this->defaultAdapter;
+ }
+
+ if (isset($this->localAdapters[$callingCode])) {
+ return $this->localAdapters[$callingCode];
+ }
+
+ return $this->defaultAdapter;
+ }
+}
diff --git a/src/Utopia/Messaging/Adapter/SMS/GEOSMS/CallingCode.php b/src/Utopia/Messaging/Adapter/SMS/GEOSMS/CallingCode.php
new file mode 100644
index 00000000..35e8fe0d
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/SMS/GEOSMS/CallingCode.php
@@ -0,0 +1,595 @@
+ true,
+ self::ANDORRA => true,
+ self::ANGOLA => true,
+ self::ARGENTINA => true,
+ self::ARMENIA => true,
+ self::ARUBA => true,
+ self::AUSTRALIA => true,
+ self::AUSTRIA => true,
+ self::AZERBAIJAN => true,
+ self::BAHRAIN => true,
+ self::BANGLADESH => true,
+ self::BELARUS => true,
+ self::BELGIUM => true,
+ self::BELIZE => true,
+ self::BENIN => true,
+ self::BHUTAN => true,
+ self::BOLIVIA => true,
+ self::BOSNIA_HERZEGOVINA => true,
+ self::BOTSWANA => true,
+ self::BRAZIL => true,
+ self::BRUNEI => true,
+ self::BULGARIA => true,
+ self::BURKINA_FASO => true,
+ self::BURUNDI => true,
+ self::CAMBODIA => true,
+ self::CAMEROON => true,
+ self::CAPE_VERDE_ISLANDS => true,
+ self::CENTRAL_AFRICAN_REPUBLIC => true,
+ self::CHILE => true,
+ self::CHINA => true,
+ self::COLOMBIA => true,
+ self::COMOROS_AND_MAYOTTE => true,
+ self::CONGO => true,
+ self::COOK_ISLANDS => true,
+ self::COSTA_RICA => true,
+ self::CROATIA => true,
+ self::CUBA => true,
+ self::CYPRUS => true,
+ self::CZECH_REPUBLIC => true,
+ self::DENMARK => true,
+ self::DJIBOUTI => true,
+ self::ECUADOR => true,
+ self::EGYPT => true,
+ self::EL_SALVADOR => true,
+ self::EQUATORIAL_GUINEA => true,
+ self::ERITREA => true,
+ self::ESTONIA => true,
+ self::ETHIOPIA => true,
+ self::FALKLAND_ISLANDS => true,
+ self::FAROE_ISLANDS => true,
+ self::FIJI => true,
+ self::FINLAND => true,
+ self::FRANCE => true,
+ self::FRENCH_GUIANA => true,
+ self::FRENCH_POLYNESIA => true,
+ self::GABON => true,
+ self::GAMBIA => true,
+ self::GEORGIA => true,
+ self::GERMANY => true,
+ self::GHANA => true,
+ self::GIBRALTAR => true,
+ self::GREECE => true,
+ self::GREENLAND => true,
+ self::GUADELOUPE => true,
+ self::GUAM => true,
+ self::GUATEMALA => true,
+ self::GUINEA => true,
+ self::GUINEA_BISSAU => true,
+ self::GUYANA => true,
+ self::HAITI => true,
+ self::HONDURAS => true,
+ self::HONG_KONG => true,
+ self::HUNGARY => true,
+ self::ICELAND => true,
+ self::INDIA => true,
+ self::INDONESIA => true,
+ self::IRAN => true,
+ self::IRAQ => true,
+ self::IRELAND => true,
+ self::ISRAEL => true,
+ self::ITALY => true,
+ self::JAPAN => true,
+ self::JORDAN => true,
+ self::KENYA => true,
+ self::KIRIBATI => true,
+ self::NORTH_KOREA => true,
+ self::SOUTH_KOREA => true,
+ self::KUWAIT => true,
+ self::KYRGYZSTAN => true,
+ self::LAOS => true,
+ self::LATVIA => true,
+ self::LEBANON => true,
+ self::LESOTHO => true,
+ self::LIBERIA => true,
+ self::LIBYA => true,
+ self::LIECHTENSTEIN => true,
+ self::LITHUANIA => true,
+ self::LUXEMBOURG => true,
+ self::MACAO => true,
+ self::MACEDONIA => true,
+ self::MADAGASCAR => true,
+ self::MALAWI => true,
+ self::MALAYSIA => true,
+ self::MALDIVES => true,
+ self::MALI => true,
+ self::MALTA => true,
+ self::MARSHALL_ISLANDS => true,
+ self::MARTINIQUE => true,
+ self::MAURITANIA => true,
+ self::MEXICO => true,
+ self::MICRONESIA => true,
+ self::MOLDOVA => true,
+ self::MONACO => true,
+ self::MONGOLIA => true,
+ self::MOROCCO => true,
+ self::MOZAMBIQUE => true,
+ self::MYANMAR => true,
+ self::NAMIBIA => true,
+ self::NAURU => true,
+ self::NEPAL => true,
+ self::NETHERLANDS => true,
+ self::NEW_CALEDONIA => true,
+ self::NEW_ZEALAND => true,
+ self::NICARAGUA => true,
+ self::NIGER => true,
+ self::NIGERIA => true,
+ self::NIUE => true,
+ self::NORFOLK_ISLANDS => true,
+ self::NORTHERN_MARIANA_ISLANDS => true,
+ self::NORWAY => true,
+ self::OMAN => true,
+ self::PALAU => true,
+ self::PANAMA => true,
+ self::PAPUA_NEW_GUINEA => true,
+ self::PARAGUAY => true,
+ self::PERU => true,
+ self::PHILIPPINES => true,
+ self::POLAND => true,
+ self::PORTUGAL => true,
+ self::QATAR => true,
+ self::REUNION => true,
+ self::ROMANIA => true,
+ self::RUSSIA_KAZAKHSTAN_UZBEKISTAN_TURKMENISTAN_AND_TAJIKSTAN => true,
+ self::RWANDA => true,
+ self::SAN_MARINO => true,
+ self::SAO_TOME_AND_PRINCIPE => true,
+ self::SAUDI_ARABIA => true,
+ self::SENEGAL => true,
+ self::SERBIA => true,
+ self::SEYCHELLES => true,
+ self::SIERRA_LEONE => true,
+ self::SINGAPORE => true,
+ self::SLOVAK_REPUBLIC => true,
+ self::SLOVENIA => true,
+ self::SOLOMON_ISLANDS => true,
+ self::SOMALIA => true,
+ self::SOUTH_AFRICA => true,
+ self::SPAIN => true,
+ self::SRI_LANKA => true,
+ self::ST_HELENA => true,
+ self::SUDAN => true,
+ self::SURINAME => true,
+ self::SWAZILAND => true,
+ self::SWEDEN => true,
+ self::SWITZERLAND => true,
+ self::SYRIA => true,
+ self::TAIWAN => true,
+ self::THAILAND => true,
+ self::TOGO => true,
+ self::TONGA => true,
+ self::TUNISIA => true,
+ self::TURKEY => true,
+ self::TUVALU => true,
+ self::UGANDA => true,
+ self::UKRAINE => true,
+ self::UNITED_ARAB_EMIRATES => true,
+ self::UNITED_KINGDOM => true,
+ self::URUGUAY => true,
+ self::NORTH_AMERICA => true,
+ self::VANUATU => true,
+ self::VENEZUELA => true,
+ self::VIETNAM => true,
+ self::WALLIS_AND_FUTUNA => true,
+ self::YEMEN => true,
+ self::ZAMBIA => true,
+ self::ZANZIBAR => true,
+ self::ZIMBABWE => true,
+ ];
+
+ public static function fromPhoneNumber(string $number): ?string
+ {
+ $digits = str_replace(['+', ' ', '(', ')', '-'], '', $number);
+
+ // Remove international call prefix, usually `00` or `011`
+ // https://en.wikipedia.org/wiki/List_of_international_call_prefixes
+ $digits = preg_replace('/^00|^011/', '', $digits);
+
+ // Prefixes can be 3, 2, or 1 digits long
+ // Attempt to match the longest first
+ foreach ([3, 2, 1] as $length) {
+ $code = substr($digits, 0, $length);
+ if (isset(self::CODES[$code])) {
+ return $code;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/Utopia/Messaging/Adapter/SMS/Infobip.php b/src/Utopia/Messaging/Adapter/SMS/Infobip.php
new file mode 100644
index 00000000..f52e1079
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/SMS/Infobip.php
@@ -0,0 +1,58 @@
+ ['to' => \ltrim($number, '+')], $message->getTo());
+
+ return $this->request(
+ method: 'POST',
+ url: "https://{$this->apiBaseUrl}/sms/2/text/advanced",
+ headers: [
+ 'Authorization: App '.$this->apiKey,
+ 'content-type: application/json',
+ ],
+ body: \json_encode([
+ 'messages' => [
+ 'text' => $message->getContent(),
+ 'from' => $this->from ?? $message->getFrom(),
+ 'destinations' => $to,
+ ],
+ ]),
+ );
+ }
+}
diff --git a/src/Utopia/Messaging/Adapters/SMS/Mock.php b/src/Utopia/Messaging/Adapter/SMS/Mock.php
similarity index 79%
rename from src/Utopia/Messaging/Adapters/SMS/Mock.php
rename to src/Utopia/Messaging/Adapter/SMS/Mock.php
index 8a29890b..a2a4a474 100644
--- a/src/Utopia/Messaging/Adapters/SMS/Mock.php
+++ b/src/Utopia/Messaging/Adapter/SMS/Mock.php
@@ -1,15 +1,15 @@
user}",
"x-key: {$this->secret}",
],
diff --git a/src/Utopia/Messaging/Adapters/SMS/Msg91.php b/src/Utopia/Messaging/Adapter/SMS/Msg91.php
similarity index 55%
rename from src/Utopia/Messaging/Adapters/SMS/Msg91.php
rename to src/Utopia/Messaging/Adapter/SMS/Msg91.php
index a83e143d..9f9039ae 100644
--- a/src/Utopia/Messaging/Adapters/SMS/Msg91.php
+++ b/src/Utopia/Messaging/Adapter/SMS/Msg91.php
@@ -1,18 +1,20 @@
templateId = $templateId;
+
+ return $this;
+ }
+
/**
- * @inheritdoc
+ * {@inheritdoc}
+ *
* @throws \Exception
*/
protected function process(SMS $message): string
{
- $to = \array_map(
- fn($to) => ['mobiles' => \ltrim($to, '+')],
- $message->getTo()
- );
+ $recipients = [];
+ foreach ($message->getTo() as $recipient) {
+ $recipients[] = [
+ 'mobiles' => \ltrim($recipient, '+'),
+ 'content' => $message->getContent(),
+ 'otp' => $message->getContent(),
+ ];
+ }
return $this->request(
method: 'POST',
url: 'https://api.msg91.com/api/v5/flow/',
headers: [
- "content-type: application/json",
+ 'content-type: application/json',
"authkey: {$this->authKey}",
],
body: \json_encode([
'sender' => $this->senderId,
- 'otp' => $message->getContent(),
- 'flow_id' => $message->getFrom(),
- 'recipients' => [$to]
+ 'template_id' => $this->templateId,
+ 'recipients' => $recipients,
]),
);
}
diff --git a/src/Utopia/Messaging/Adapter/SMS/Plivo.php b/src/Utopia/Messaging/Adapter/SMS/Plivo.php
new file mode 100644
index 00000000..4d285692
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/SMS/Plivo.php
@@ -0,0 +1,53 @@
+request(
+ method: 'POST',
+ url: "https://api.plivo.com/v1/Account/{$this->authId}/Message/",
+ headers: [
+ 'Authorization: Basic '.base64_encode("{$this->authId}:{$this->authToken}"),
+ ],
+ body: \http_build_query([
+ 'text' => $message->getContent(),
+ 'src' => $this->from ?? $message->getFrom() ?? 'Plivo',
+ 'dst' => \implode('<', $message->getTo()),
+ ]),
+ );
+ }
+}
diff --git a/src/Utopia/Messaging/Adapter/SMS/Seven.php b/src/Utopia/Messaging/Adapter/SMS/Seven.php
new file mode 100644
index 00000000..4f388c58
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/SMS/Seven.php
@@ -0,0 +1,52 @@
+request(
+ method: 'POST',
+ url: 'https://gateway.sms77.io/api/sms',
+ headers: [
+ 'Authorization: Basic '.$this->apiKey,
+ 'content-type: application/json',
+ ],
+ body: \json_encode([
+ 'from' => $this->from ?? $message->getFrom(),
+ 'to' => \implode(',', $message->getTo()),
+ 'text' => $message->getContent(),
+ ]),
+ );
+ }
+}
diff --git a/src/Utopia/Messaging/Adapter/SMS/Sinch.php b/src/Utopia/Messaging/Adapter/SMS/Sinch.php
new file mode 100644
index 00000000..6f2d1773
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/SMS/Sinch.php
@@ -0,0 +1,56 @@
+ \ltrim($number, '+'), $message->getTo());
+
+ return $this->request(
+ method: 'POST',
+ url: "https://sms.api.sinch.com/xms/v1/{$this->servicePlanId}/batches",
+ headers: [
+ 'Authorization: Bearer '.$this->apiToken,
+ 'content-type: application/json',
+ ],
+ body: \json_encode([
+ 'from' => $this->from ?? $message->getFrom(),
+ 'to' => $to,
+ 'body' => $message->getContent(),
+ ]),
+ );
+ }
+}
diff --git a/src/Utopia/Messaging/Adapters/SMS/Telesign.php b/src/Utopia/Messaging/Adapter/SMS/Telesign.php
similarity index 51%
rename from src/Utopia/Messaging/Adapters/SMS/Telesign.php
rename to src/Utopia/Messaging/Adapter/SMS/Telesign.php
index 7cef0f45..896e90d2 100644
--- a/src/Utopia/Messaging/Adapters/SMS/Telesign.php
+++ b/src/Utopia/Messaging/Adapter/SMS/Telesign.php
@@ -1,9 +1,9 @@
\ltrim($to, '+'),
+ $to = $this->formatNumbers(\array_map(
+ fn ($to) => $to,
$message->getTo()
- );
+ ));
return $this->request(
method: 'POST',
url: 'https://rest-ww.telesign.com/v1/verify/bulk_sms',
headers: [
- 'Authorization: Basic ' . base64_encode("{$this->username}:{$this->password}")
+ 'Authorization: Basic '.base64_encode("{$this->username}:{$this->password}"),
],
- body: [
+ body: \http_build_query([
'template' => $message->getContent(),
- 'recipients' => \implode(',', $to)
- ],
+ 'recipients' => $to,
+ ]),
);
}
+
+ /**
+ * @param array $numbers
+ */
+ private function formatNumbers(array $numbers): string
+ {
+ $formatted = \array_map(
+ fn ($number) => $number.':'.\uniqid(),
+ $numbers
+ );
+
+ return implode(',', $formatted);
+ }
}
diff --git a/src/Utopia/Messaging/Adapter/SMS/Telnyx.php b/src/Utopia/Messaging/Adapter/SMS/Telnyx.php
new file mode 100644
index 00000000..0a6b5827
--- /dev/null
+++ b/src/Utopia/Messaging/Adapter/SMS/Telnyx.php
@@ -0,0 +1,50 @@
+request(
+ method: 'POST',
+ url: 'https://api.telnyx.com/v2/messages',
+ headers: [
+ 'Authorization: Bearer '.$this->apiKey,
+ 'Content-Type: application/json',
+ ],
+ body: \json_encode([
+ 'text' => $message->getContent(),
+ 'from' => $this->from ?? $message->getFrom(),
+ 'to' => $message->getTo()[0],
+ ]),
+ );
+ }
+}
diff --git a/src/Utopia/Messaging/Adapters/SMS/TextMagic.php b/src/Utopia/Messaging/Adapter/SMS/TextMagic.php
similarity index 67%
rename from src/Utopia/Messaging/Adapters/SMS/TextMagic.php
rename to src/Utopia/Messaging/Adapter/SMS/TextMagic.php
index a78a97dc..69c52a46 100644
--- a/src/Utopia/Messaging/Adapters/SMS/TextMagic.php
+++ b/src/Utopia/Messaging/Adapter/SMS/TextMagic.php
@@ -1,22 +1,23 @@
\ltrim($to, '+'),
+ fn ($to) => \ltrim($to, '+'),
$message->getTo()
);
@@ -48,11 +50,11 @@ protected function process(SMS $message): string
"X-TM-Username: {$this->username}",
"X-TM-Key: {$this->apiKey}",
],
- body: [
+ body: \http_build_query([
'text' => $message->getContent(),
- 'from' => \ltrim($message->getFrom(), '+'),
+ 'from' => \ltrim($this->from ?? $message->getFrom(), '+'),
'phones' => \implode(',', $to),
- ],
+ ]),
);
}
}
diff --git a/src/Utopia/Messaging/Adapters/SMS/Twilio.php b/src/Utopia/Messaging/Adapter/SMS/Twilio.php
similarity index 60%
rename from src/Utopia/Messaging/Adapters/SMS/Twilio.php
rename to src/Utopia/Messaging/Adapter/SMS/Twilio.php
index 9a65225a..f3ea3e65 100644
--- a/src/Utopia/Messaging/Adapters/SMS/Twilio.php
+++ b/src/Utopia/Messaging/Adapter/SMS/Twilio.php
@@ -1,19 +1,20 @@
accountSid}/Messages.json",
headers: [
- 'Authorization: Basic ' . base64_encode("{$this->accountSid}:{$this->authToken}")
+ 'Authorization: Basic '.base64_encode("{$this->accountSid}:{$this->authToken}"),
],
body: \http_build_query([
'Body' => $message->getContent(),
- 'From' => $message->getFrom(),
- 'To' => $message->getTo()[0]
+ 'From' => $this->from ?? $message->getFrom(),
+ 'To' => $message->getTo()[0],
]),
);
}
diff --git a/src/Utopia/Messaging/Adapters/SMS/Vonage.php b/src/Utopia/Messaging/Adapter/SMS/Vonage.php
similarity index 60%
rename from src/Utopia/Messaging/Adapters/SMS/Vonage.php
rename to src/Utopia/Messaging/Adapter/SMS/Vonage.php
index 598029ea..814d582f 100644
--- a/src/Utopia/Messaging/Adapters/SMS/Vonage.php
+++ b/src/Utopia/Messaging/Adapter/SMS/Vonage.php
@@ -1,22 +1,23 @@
\ltrim($to, '+'),
+ fn ($to) => \ltrim($to, '+'),
$message->getTo()
);
return $this->request(
method: 'POST',
url: 'https://rest.nexmo.com/sms/json',
- body: [
+ body: \http_build_query([
'text' => $message->getContent(),
- 'from' => $message->getFrom(),
- 'to' => \implode(',', $to),
+ 'from' => $this->from ?? $message->getFrom(),
+ 'to' => $to[0], //\implode(',', $to),
'api_key' => $this->apiKey,
- 'api_secret' => $this->apiSecret
- ]
+ 'api_secret' => $this->apiSecret,
+ ]),
);
}
}
diff --git a/src/Utopia/Messaging/Adapters/Email.php b/src/Utopia/Messaging/Adapters/Email.php
deleted file mode 100644
index 4cfff3fc..00000000
--- a/src/Utopia/Messaging/Adapters/Email.php
+++ /dev/null
@@ -1,43 +0,0 @@
-getMessageType())) {
- throw new \Exception('Invalid message type.');
- }
- if (\count($message->getTo()) > $this->getMaxMessagesPerRequest()) {
- throw new \Exception("{$this->getName()} can only send {$this->getMaxMessagesPerRequest()} messages per request.");
- }
- return $this->process($message);
- }
-
- /**
- * Process an email message.
- *
- * @param EmailMessage $message Message to process.
- * @return string The response body.
- */
- abstract protected function process(EmailMessage $message): string;
-}
diff --git a/src/Utopia/Messaging/Adapters/Push.php b/src/Utopia/Messaging/Adapters/Push.php
deleted file mode 100644
index 419fc987..00000000
--- a/src/Utopia/Messaging/Adapters/Push.php
+++ /dev/null
@@ -1,43 +0,0 @@
-getMessageType())) {
- throw new \Exception('Invalid message type.');
- }
- if (\count($message->getTo()) > $this->getMaxMessagesPerRequest()) {
- throw new \Exception("{$this->getName()} can only send {$this->getMaxMessagesPerRequest()} messages per request.");
- }
- return $this->process($message);
- }
-
- /**
- * Send a push message.
- *
- * @param PushMessage $message Message to process.
- * @return string The response body.
- */
- abstract protected function process(PushMessage $message): string;
-}
diff --git a/src/Utopia/Messaging/Adapters/SMS.php b/src/Utopia/Messaging/Adapters/SMS.php
deleted file mode 100644
index 7b28dff8..00000000
--- a/src/Utopia/Messaging/Adapters/SMS.php
+++ /dev/null
@@ -1,43 +0,0 @@
-getMessageType())) {
- throw new \Exception('Invalid message type.');
- }
- if (\count($message->getTo()) > $this->getMaxMessagesPerRequest()) {
- throw new \Exception("{$this->getName()} can only send {$this->getMaxMessagesPerRequest()} messages per request.");
- }
- return $this->process($message);
- }
-
- /**
- * Send an SMS message.
- *
- * @param SMSMessage $message Message to send.
- * @return string The response body.
- */
- abstract protected function process(SMSMessage $message): string;
-}
diff --git a/src/Utopia/Messaging/Adapters/SMS/TwilioNotify.php b/src/Utopia/Messaging/Adapters/SMS/TwilioNotify.php
deleted file mode 100644
index 64e07d8c..00000000
--- a/src/Utopia/Messaging/Adapters/SMS/TwilioNotify.php
+++ /dev/null
@@ -1,52 +0,0 @@
-request(
- method: 'POST',
- url: "https://notify.twilio.com/v1/Services/{$this->serviceSid}/Notifications",
- headers: [
- 'Authorization: Basic ' . base64_encode("{$this->accountSid}:{$this->authToken}")
- ],
- body: \http_build_query([
- 'Body' => $message->getContent(),
- 'ToBinding' => \json_encode(\array_map(
- fn($to) => ['binding_type' => 'sms', 'address' => $to],
- $message->getTo()
- )),
- ]),
- );
- }
-}
diff --git a/src/Utopia/Messaging/Messages/Discord.php b/src/Utopia/Messaging/Messages/Discord.php
new file mode 100644
index 00000000..dffa5fec
--- /dev/null
+++ b/src/Utopia/Messaging/Messages/Discord.php
@@ -0,0 +1,102 @@
+|null $embeds
+ * @param array|null $allowedMentions
+ * @param array|null $components
+ * @param array|null $attachments
+ */
+ public function __construct(
+ private string $content,
+ private ?string $username = null,
+ private ?string $avatarUrl = null,
+ private ?bool $tts = null,
+ private ?array $embeds = null,
+ private ?array $allowedMentions = null,
+ private ?array $components = null,
+ private ?array $attachments = null,
+ private ?string $flags = null,
+ private ?string $threadName = null,
+ private ?bool $wait = null,
+ private ?string $threadId = null
+ ) {
+ }
+
+ public function getContent(): string
+ {
+ return $this->content;
+ }
+
+ public function getUsername(): ?string
+ {
+ return $this->username;
+ }
+
+ public function getAvatarUrl(): ?string
+ {
+ return $this->avatarUrl;
+ }
+
+ public function getTts(): ?bool
+ {
+ return $this->tts;
+ }
+
+ /**
+ * @return array|null
+ */
+ public function getEmbeds(): ?array
+ {
+ return $this->embeds;
+ }
+
+ /**
+ * @return array|null
+ */
+ public function getAllowedMentions(): ?array
+ {
+ return $this->allowedMentions;
+ }
+
+ /**
+ * @return array|null
+ */
+ public function getComponents(): ?array
+ {
+ return $this->components;
+ }
+
+ /**
+ * @return array|null
+ */
+ public function getAttachments(): ?array
+ {
+ return $this->attachments;
+ }
+
+ public function getFlags(): ?string
+ {
+ return $this->flags;
+ }
+
+ public function getThreadName(): ?string
+ {
+ return $this->threadName;
+ }
+
+ public function getWait(): ?bool
+ {
+ return $this->wait;
+ }
+
+ public function getThreadId(): ?string
+ {
+ return $this->threadId;
+ }
+}
diff --git a/src/Utopia/Messaging/Messages/Email.php b/src/Utopia/Messaging/Messages/Email.php
index e13ab30a..8e79c10c 100644
--- a/src/Utopia/Messaging/Messages/Email.php
+++ b/src/Utopia/Messaging/Messages/Email.php
@@ -7,12 +7,12 @@
class Email implements Message
{
/**
- * @param array $to The recipients of the email.
- * @param string $subject The subject of the email.
- * @param string $content The content of the email.
- * @param string|null $from The sender of the email.
- * @param array|null $attachments The attachments of the email.
- * @param bool $html Whether the message is HTML or not.
+ * @param array $to The recipients of the email.
+ * @param string $subject The subject of the email.
+ * @param string $content The content of the email.
+ * @param string|null $from The sender of the email.
+ * @param array|null $attachments The attachments of the email.
+ * @param bool $html Whether the message is HTML or not.
*/
public function __construct(
private array $to,
@@ -25,48 +25,36 @@ public function __construct(
}
/**
- * @return array
+ * @return array
*/
public function getTo(): array
{
return $this->to;
}
- /**
- * @return string
- */
public function getSubject(): string
{
return $this->subject;
}
- /**
- * @return string
- */
public function getContent(): string
{
return $this->content;
}
- /**
- * @return string|null
- */
public function getFrom(): ?string
{
return $this->from;
}
/**
- * @return array|null
+ * @return array|null
*/
public function getAttachments(): ?array
{
return $this->attachments;
}
- /**
- * @return bool
- */
public function isHtml(): bool
{
return $this->html;
diff --git a/src/Utopia/Messaging/Messages/Push.php b/src/Utopia/Messaging/Messages/Push.php
index 4a86a05e..bf1712f6 100644
--- a/src/Utopia/Messaging/Messages/Push.php
+++ b/src/Utopia/Messaging/Messages/Push.php
@@ -7,16 +7,16 @@
class Push implements Message
{
/**
- * @param array $to The recipients of the push notification.
- * @param string $title The title of the push notification.
- * @param string $body The body of the push notification.
- * @param array|null $data This parameter specifies the custom key-value pairs of the message's payload. For example, with data:{"score":"3x1"}:
On Apple platforms, if the message is sent via APNs, it represents the custom data fields. If it is sent via FCM, it would be represented as key value dictionary in AppDelegate application:didReceiveRemoteNotification:.
On Android, this would result in an intent extra named score with the string value 3x1.
The key should not be a reserved word ("from", "message_type", or any word starting with "google" or "gcm"). Do not use any of the words defined in this table (such as collapse_key).
Values in string types are recommended. You have to convert values in objects or other non-string data types (e.g., integers or booleans) to string.
- * @param string|null $sound The sound to play when the device receives the notification.
On Android, sound files must reside in /res/raw/.
On iOS, sounds files must reside in the main bundle of the client app or in the Library/Sounds folder of the app's data container.
- * @param string|null $action The action associated with a user click on the notification.
On Android, this is the activity to launch.
On iOS, this is the category to launch.
- * @param string|null $icon Android only. The icon of the push notification. Sets the notification icon to myicon for drawable resource myicon. If you don't send this key in the request, FCM displays the launcher icon specified in your app manifest.
- * @param string|null $color Android only. The icon color of the push notification, expressed in #rrggbb format.
- * @param string|null $tag Android only. Identifier used to replace existing notifications in the notification drawer.
If not specified, each request creates a new notification.
If specified and a notification with the same tag is already being shown, the new notification replaces the existing one in the notification drawer.
- * @param string|null $badge iOS only. The value of the badge on the home screen app icon. If not specified, the badge is not changed. If set to 0, the badge is removed.
+ * @param array $to The recipients of the push notification.
+ * @param string $title The title of the push notification.
+ * @param string $body The body of the push notification.
+ * @param array|null $data This parameter specifies the custom key-value pairs of the message's payload. For example, with data:{"score":"3x1"}:
On Apple platforms, if the message is sent via APNs, it represents the custom data fields. If it is sent via FCM, it would be represented as key value dictionary in AppDelegate application:didReceiveRemoteNotification:.
On Android, this would result in an intent extra named score with the string value 3x1.
The key should not be a reserved word ("from", "message_type", or any word starting with "google" or "gcm"). Do not use any of the words defined in this table (such as collapse_key).
Values in string types are recommended. You have to convert values in objects or other non-string data types (e.g., integers or booleans) to string.
+ * @param string|null $sound The sound to play when the device receives the notification.
On Android, sound files must reside in /res/raw/.
On iOS, sounds files must reside in the main bundle of the client app or in the Library/Sounds folder of the app's data container.
+ * @param string|null $action The action associated with a user click on the notification.
On Android, this is the activity to launch.
On iOS, this is the category to launch.
+ * @param string|null $icon Android only. The icon of the push notification. Sets the notification icon to myicon for drawable resource myicon. If you don't send this key in the request, FCM displays the launcher icon specified in your app manifest.
+ * @param string|null $color Android only. The icon color of the push notification, expressed in #rrggbb format.
+ * @param string|null $tag Android only. Identifier used to replace existing notifications in the notification drawer.
If not specified, each request creates a new notification.
If specified and a notification with the same tag is already being shown, the new notification replaces the existing one in the notification drawer.
+ * @param string|null $badge iOS only. The value of the badge on the home screen app icon. If not specified, the badge is not changed. If set to 0, the badge is removed.
*/
public function __construct(
private array $to,
@@ -33,80 +33,61 @@ public function __construct(
}
/**
- * @return array
+ * @return array
*/
public function getTo(): array
{
return $this->to;
}
- /**
- * @return string
- */
+ public function getFrom(): ?string
+ {
+ return null;
+ }
+
public function getTitle(): string
{
return $this->title;
}
- /**
- * @return string
- */
public function getBody(): string
{
return $this->body;
}
/**
- * @return array|null
+ * @return array|null
*/
public function getData(): ?array
{
return $this->data;
}
- /**
- * @return string|null
- */
public function getAction(): ?string
{
return $this->action;
}
- /**
- * @return string|null
- */
public function getSound(): ?string
{
return $this->sound;
}
- /**
- * @return string|null
- */
public function getIcon(): ?string
{
return $this->icon;
}
- /**
- * @return string|null
- */
public function getColor(): ?string
{
return $this->color;
}
- /**
- * @return string|null
- */
public function getTag(): ?string
{
return $this->tag;
}
- /**
- * @return string|null
- */
public function getBadge(): ?string
{
return $this->badge;
diff --git a/src/Utopia/Messaging/Messages/SMS.php b/src/Utopia/Messaging/Messages/SMS.php
index bcb913c0..a2eb2bad 100644
--- a/src/Utopia/Messaging/Messages/SMS.php
+++ b/src/Utopia/Messaging/Messages/SMS.php
@@ -6,6 +6,10 @@
class SMS implements Message
{
+ /**
+ * @param array $to
+ * @param array|null $attachments
+ */
public function __construct(
private array $to,
private string $content,
@@ -15,31 +19,25 @@ public function __construct(
}
/**
- * @return array
+ * @return array
*/
public function getTo(): array
{
return $this->to;
}
- /**
- * @return string
- */
public function getContent(): string
{
return $this->content;
}
- /**
- * @return string|null
- */
public function getFrom(): ?string
{
return $this->from;
}
/**
- * @return array|null
+ * @return array|null
*/
public function getAttachments(): ?array
{
diff --git a/tests/e2e/Base.php b/tests/Messaging/Adapter/Base.php
similarity index 82%
rename from tests/e2e/Base.php
rename to tests/Messaging/Adapter/Base.php
index 14970e51..218cadd9 100644
--- a/tests/e2e/Base.php
+++ b/tests/Messaging/Adapter/Base.php
@@ -1,11 +1,14 @@
+ */
protected function getLastRequest(): array
{
\sleep(2);
@@ -16,6 +19,9 @@ protected function getLastRequest(): array
return $request;
}
+ /**
+ * @return array
+ */
protected function getLastEmail(): array
{
sleep(3);
diff --git a/tests/Messaging/Adapter/Chat/DiscordTest.php b/tests/Messaging/Adapter/Chat/DiscordTest.php
new file mode 100644
index 00000000..ee7df927
--- /dev/null
+++ b/tests/Messaging/Adapter/Chat/DiscordTest.php
@@ -0,0 +1,33 @@
+send($message), true);
+
+ $this->assertNotEmpty($result);
+ $this->assertNotEmpty($result['id']);
+ }
+}
diff --git a/tests/e2e/Email/EmailTest.php b/tests/Messaging/Adapter/Email/EmailTest.php
similarity index 84%
rename from tests/e2e/Email/EmailTest.php
rename to tests/Messaging/Adapter/Email/EmailTest.php
index 40606255..f0064acd 100644
--- a/tests/e2e/Email/EmailTest.php
+++ b/tests/Messaging/Adapter/Email/EmailTest.php
@@ -1,16 +1,17 @@
send($message), true);
+
+ $this->assertArrayHasKey('id', $result);
+ $this->assertArrayHasKey('message', $result);
+ $this->assertTrue(\str_contains(\strtolower($result['message']), 'queued'));
+ }
+}
diff --git a/tests/Messaging/Adapter/Email/SendgridTest.php b/tests/Messaging/Adapter/Email/SendgridTest.php
new file mode 100644
index 00000000..58c10b66
--- /dev/null
+++ b/tests/Messaging/Adapter/Email/SendgridTest.php
@@ -0,0 +1,39 @@
+send($message);
+
+ $this->assertEquals($response, '');
+ */
+
+ $this->markTestSkipped('Sendgrid: Authenticated user is not authorized to send mail');
+ }
+}
diff --git a/tests/Messaging/Adapter/Push/APNSTest.php b/tests/Messaging/Adapter/Push/APNSTest.php
new file mode 100644
index 00000000..dbbf6874
--- /dev/null
+++ b/tests/Messaging/Adapter/Push/APNSTest.php
@@ -0,0 +1,39 @@
+send($message));
+
+ foreach ($responses as $response) {
+ $this->assertEquals('success', $response->status);
+ }
+ }
+}
diff --git a/tests/Messaging/Adapter/Push/FCMTest.php b/tests/Messaging/Adapter/Push/FCMTest.php
new file mode 100644
index 00000000..3008962b
--- /dev/null
+++ b/tests/Messaging/Adapter/Push/FCMTest.php
@@ -0,0 +1,38 @@
+send($message));
+
+ $this->assertNotEmpty($response);
+ $this->assertEquals(1, $response->success);
+ $this->assertEquals(0, $response->failure);
+ }
+}
diff --git a/tests/Messaging/Adapter/SMS/GEOSMS/CallingCodeTest.php b/tests/Messaging/Adapter/SMS/GEOSMS/CallingCodeTest.php
new file mode 100644
index 00000000..cd1321be
--- /dev/null
+++ b/tests/Messaging/Adapter/SMS/GEOSMS/CallingCodeTest.php
@@ -0,0 +1,19 @@
+assertEquals(CallingCode::NORTH_AMERICA, CallingCode::fromPhoneNumber('+11234567890'));
+ $this->assertEquals(CallingCode::INDIA, CallingCode::fromPhoneNumber('+911234567890'));
+ $this->assertEquals(CallingCode::ISRAEL, CallingCode::fromPhoneNumber('9721234567890'));
+ $this->assertEquals(CallingCode::UNITED_ARAB_EMIRATES, CallingCode::fromPhoneNumber('009711234567890'));
+ $this->assertEquals(CallingCode::UNITED_KINGDOM, CallingCode::fromPhoneNumber('011441234567890'));
+ $this->assertEquals(null, CallingCode::fromPhoneNumber('2'));
+ }
+}
diff --git a/tests/Messaging/Adapter/SMS/GEOSMSTest.php b/tests/Messaging/Adapter/SMS/GEOSMSTest.php
new file mode 100644
index 00000000..d10b7799
--- /dev/null
+++ b/tests/Messaging/Adapter/SMS/GEOSMSTest.php
@@ -0,0 +1,124 @@
+createMock(SMSAdapter::class);
+ $defaultAdapterMock->method('getName')
+ ->willReturn('default');
+ $defaultAdapterMock->method('send')
+ ->willReturn(json_encode(['status' => 'success']));
+
+ $adapter = new GEOSMS($defaultAdapterMock);
+
+ $to = ['+11234567890'];
+ $from = 'Sender';
+
+ $message = new SMS(
+ to: $to,
+ content: 'Test Content',
+ from: $from
+ );
+
+ $result = json_decode($adapter->send($message), true);
+
+ $this->assertEquals(1, count($result));
+ $this->assertEquals('success', $result['default']['status']);
+ }
+
+ public function testSendSMSUsingLocalAdapter(): void
+ {
+ $defaultAdapterMock = $this->createMock(SMSAdapter::class);
+ $localAdapterMock = $this->createMock(SMSAdapter::class);
+ $localAdapterMock->method('getName')
+ ->willReturn('local');
+ $localAdapterMock->method('send')
+ ->willReturn(json_encode(['status' => 'success']));
+
+ $adapter = new GEOSMS($defaultAdapterMock);
+ $adapter->setLocal(CallingCode::INDIA, $localAdapterMock);
+
+ $to = ['+911234567890'];
+ $from = 'Sender';
+
+ $message = new SMS(
+ to: $to,
+ content: 'Test Content',
+ from: $from
+ );
+
+ $result = json_decode($adapter->send($message), true);
+
+ $this->assertEquals(1, count($result));
+ $this->assertEquals('success', $result['local']['status']);
+ }
+
+ public function testSendSMSUsingLocalAdapterAndDefault(): void
+ {
+ $defaultAdapterMock = $this->createMock(SMSAdapter::class);
+ $defaultAdapterMock->method('getName')
+ ->willReturn('default');
+ $defaultAdapterMock->method('send')
+ ->willReturn(json_encode(['status' => 'success']));
+ $localAdapterMock = $this->createMock(SMSAdapter::class);
+ $localAdapterMock->method('getName')
+ ->willReturn('local');
+ $localAdapterMock->method('send')
+ ->willReturn(json_encode(['status' => 'success']));
+
+ $adapter = new GEOSMS($defaultAdapterMock);
+ $adapter->setLocal(CallingCode::INDIA, $localAdapterMock);
+
+ $to = ['+911234567890', '+11234567890'];
+ $from = 'Sender';
+
+ $message = new SMS(
+ to: $to,
+ content: 'Test Content',
+ from: $from
+ );
+
+ $result = json_decode($adapter->send($message), true);
+
+ $this->assertEquals(2, count($result));
+ $this->assertEquals('success', $result['local']['status']);
+ $this->assertEquals('success', $result['default']['status']);
+ }
+
+ public function testSendSMSUsingGroupedLocalAdapter(): void
+ {
+ $defaultAdapterMock = $this->createMock(SMSAdapter::class);
+ $localAdapterMock = $this->createMock(SMSAdapter::class);
+ $localAdapterMock->method('getName')
+ ->willReturn('local');
+ $localAdapterMock->method('send')
+ ->willReturn(json_encode(['status' => 'success']));
+
+ $adapter = new GEOSMS($defaultAdapterMock);
+ $adapter->setLocal(CallingCode::INDIA, $localAdapterMock);
+ $adapter->setLocal(CallingCode::NORTH_AMERICA, $localAdapterMock);
+
+ $to = ['+911234567890', '+11234567890'];
+ $from = 'Sender';
+
+ $message = new SMS(
+ to: $to,
+ content: 'Test Content',
+ from: $from
+ );
+
+ $result = json_decode($adapter->send($message), true);
+
+ $this->assertEquals(1, count($result));
+ $this->assertEquals('success', $result['local']['status']);
+ }
+}
diff --git a/tests/Messaging/Adapter/SMS/Msg91Test.php b/tests/Messaging/Adapter/SMS/Msg91Test.php
new file mode 100644
index 00000000..165dd23a
--- /dev/null
+++ b/tests/Messaging/Adapter/SMS/Msg91Test.php
@@ -0,0 +1,29 @@
+send($message);
+ // $result = \json_decode($response, true);
+
+ // $this->assertEquals('success', $result['type']);
+
+ $this->markTestSkipped('Msg91 requires business verification to use template and SMS api.');
+ }
+}
diff --git a/tests/e2e/SMS/SMSTest.php b/tests/Messaging/Adapter/SMS/SMSTest.php
similarity index 86%
rename from tests/e2e/SMS/SMSTest.php
rename to tests/Messaging/Adapter/SMS/SMSTest.php
index b7cdb8be..848b034a 100644
--- a/tests/e2e/SMS/SMSTest.php
+++ b/tests/Messaging/Adapter/SMS/SMSTest.php
@@ -1,16 +1,17 @@
markTestSkipped('Telesign requires support/sales call in order to enable bulk SMS');
+ }
+}
diff --git a/tests/Messaging/Adapter/SMS/TelnyxTest.php b/tests/Messaging/Adapter/SMS/TelnyxTest.php
new file mode 100644
index 00000000..3db276a5
--- /dev/null
+++ b/tests/Messaging/Adapter/SMS/TelnyxTest.php
@@ -0,0 +1,28 @@
+send($message), true);
+
+ // $this->assertEquals('success', $result["type"]);
+
+ $this->markTestSkipped('Telnyx had no testing numbers available at this time.');
+ }
+}
diff --git a/tests/Messaging/Adapter/SMS/TwilioTest.php b/tests/Messaging/Adapter/SMS/TwilioTest.php
new file mode 100644
index 00000000..5a1b7f96
--- /dev/null
+++ b/tests/Messaging/Adapter/SMS/TwilioTest.php
@@ -0,0 +1,33 @@
+send($message));
+
+ $this->assertNotEmpty($result);
+ $this->assertEquals($to[0], $result->to);
+ $this->assertEquals($from, $result->from);
+ $this->assertNull($result->error_message);
+ }
+}
diff --git a/tests/Messaging/Adapter/SMS/VonageTest.php b/tests/Messaging/Adapter/SMS/VonageTest.php
new file mode 100644
index 00000000..19d67620
--- /dev/null
+++ b/tests/Messaging/Adapter/SMS/VonageTest.php
@@ -0,0 +1,35 @@
+send($message);
+
+ $result = \json_decode($response, true);
+
+ $this->assertArrayHasKey('messages', $result);
+ $this->assertEquals(1, count($result['messages']));
+ $this->assertEquals('1', $result['message-count']);
+ }
+}
diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep
deleted file mode 100644
index e69de29b..00000000