diff --git a/.env b/.env
index ad551e705a..f77083a035 100644
--- a/.env
+++ b/.env
@@ -87,7 +87,7 @@ _APP_LOGGING_PROVIDER=
_APP_LOGGING_CONFIG=
_APP_GRAPHQL_MAX_BATCH_SIZE=10
_APP_GRAPHQL_MAX_COMPLEXITY=250
-_APP_GRAPHQL_MAX_DEPTH=3
+_APP_GRAPHQL_MAX_DEPTH=4
_APP_DOCKER_HUB_USERNAME=
_APP_DOCKER_HUB_PASSWORD=
_APP_VCS_GITHUB_APP_NAME=
@@ -98,4 +98,7 @@ _APP_VCS_GITHUB_CLIENT_SECRET=
_APP_VCS_GITHUB_WEBHOOK_SECRET=
_APP_MIGRATIONS_FIREBASE_CLIENT_ID=
_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=
-_APP_ASSISTANT_OPENAI_API_KEY=
\ No newline at end of file
+_APP_ASSISTANT_OPENAI_API_KEY=
+_APP_MESSAGE_SMS_TEST_DSN=
+_APP_MESSAGE_EMAIL_TEST_DSN=
+_APP_MESSAGE_PUSH_TEST_DSN=
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 842d61ff1c..14e1ac5e44 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -99,6 +99,7 @@ jobs:
Users,
Webhooks,
VCS,
+ Messaging,
]
steps:
diff --git a/.gitignore b/.gitignore
index 3151de5adb..ac88830b49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ debug/
app/sdks
dev/yasd_init.php
.phpunit.result.cache
+Makefile
diff --git a/CHANGES.md b/CHANGES.md
index 83e09896c8..5634340d69 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -484,7 +484,7 @@
## Features
- Added Phone Authentication by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3357
- Added Twilio Support
- - Added TextMagic Support
+ - Added Textmagic Support
- Added Telesign Support
- Added Endpoint to create Phone Session (`POST:/v1/account/sessions/phone`)
- Added Endpoint to confirm Phone Session (`PUT:/v1/account/sessions/phone`)
diff --git a/app/config/collections.php b/app/config/collections.php
index db229ce87a..b3e3417555 100644
--- a/app/config/collections.php
+++ b/app/config/collections.php
@@ -5,7 +5,7 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
-$providers = Config::getParam('providers', []);
+$providers = Config::getParam('oAuthProviders', []);
$auth = Config::getParam('auth', []);
/**
@@ -221,6 +221,17 @@ $commonCollections = [
'array' => false,
'filters' => ['subQueryMemberships'],
],
+ [
+ '$id' => ID::custom('targets'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['subQueryTargets'],
+ ],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
@@ -1366,7 +1377,608 @@ $commonCollections = [
],
],
],
- ];
+
+ 'providers' => [
+ '$collection' => ID::custom(DATABASE::METADATA),
+ '$id' => ID::custom('providers'),
+ 'name' => 'Providers',
+ 'attributes' => [
+ [
+ '$id' => ID::custom('name'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 128,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('provider'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('type'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 128,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('enabled'),
+ 'type' => Database::VAR_BOOLEAN,
+ 'signed' => true,
+ 'size' => 0,
+ 'format' => '',
+ 'filters' => [],
+ 'required' => true,
+ 'default' => true,
+ 'array' => false,
+ ],
+ [
+ '$id' => ID::custom('credentials'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['json', 'encrypt'],
+ ],
+ [
+ '$id' => ID::custom('options'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => [],
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ [
+ '$id' => ID::custom('search'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => ['providerSearch'],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => ID::custom('_key_provider'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['provider'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC],
+ ],
+ [
+ '$id' => ID::custom('_key_name'),
+ 'type' => Database::INDEX_FULLTEXT,
+ 'attributes' => ['name'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC],
+ ],
+ [
+ '$id' => ID::custom('_key_type'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['type'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC],
+ ],
+ [
+ '$id' => ID::custom('_key_enabled_type'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['enabled','type'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC],
+ ],
+ [
+ '$id' => ID::custom('_key_search'),
+ 'type' => Database::INDEX_FULLTEXT,
+ 'attributes' => ['search'],
+ 'lengths' => [],
+ 'orders' => [],
+ ]
+ ],
+ ],
+
+ 'messages' => [
+ '$collection' => ID::custom(DATABASE::METADATA),
+ '$id' => ID::custom('messages'),
+ 'name' => 'Messages',
+ 'attributes' => [
+ [
+ '$id' => ID::custom('providerType'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('description'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 256,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('status'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => 'processing',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('data'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ [
+ '$id' => ID::custom('topics'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 21845,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => [],
+ 'array' => true,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('users'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 21845,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => [],
+ 'array' => true,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('targets'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 21845,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => [],
+ 'array' => true,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('scheduledAt'),
+ 'type' => Database::VAR_DATETIME,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => false,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['datetime'],
+ ],
+ [
+ '$id' => ID::custom('deliveredAt'),
+ 'type' => Database::VAR_DATETIME,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => false,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['datetime'],
+ ],
+ [
+ '$id' => ID::custom('deliveryErrors'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => true,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('deliveredTotal'),
+ 'type' => Database::VAR_INTEGER,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => 0,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('search'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => ['messageSearch'],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => ID::custom('_key_search'),
+ 'type' => Database::INDEX_FULLTEXT,
+ 'attributes' => ['search'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ ],
+ ],
+
+ 'topics' => [
+ '$collection' => ID::custom(DATABASE::METADATA),
+ '$id' => ID::custom('topics'),
+ 'name' => 'Topics',
+ 'attributes' => [
+ [
+ '$id' => ID::custom('name'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 128,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('description'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 2048,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('total'),
+ 'type' => Database::VAR_INTEGER,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => 0,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('targets'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['subQueryTopicTargets'],
+ ],
+ [
+ '$id' => ID::custom('search'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16384,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => ['topicSearch'],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => ID::custom('_key_name'),
+ 'type' => Database::INDEX_FULLTEXT,
+ 'attributes' => ['name'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_search'),
+ 'type' => Database::INDEX_FULLTEXT,
+ 'attributes' => ['search'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC],
+ ]
+ ],
+ ],
+
+ 'subscribers' => [
+ '$collection' => ID::custom(DATABASE::METADATA),
+ '$id' => ID::custom('subscribers'),
+ 'name' => 'Subscribers',
+ 'attributes' => [
+ [
+ '$id' => ID::custom('targetId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('targetInternalId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('userId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('userInternalId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('topicId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('topicInternalId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('providerType'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 128,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => ID::custom('_key_targetId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['targetId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_targetInternalId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['targetInternalId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_userId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['userId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_userInternalId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['userInternalId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_topicId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['topicId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_topicInternalId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['topicInternalId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ]
+ ],
+ ],
+
+ 'targets' => [
+ '$collection' => ID::custom(DATABASE::METADATA),
+ '$id' => ID::custom('targets'),
+ 'name' => 'Targets',
+ 'attributes' => [
+ [
+ '$id' => ID::custom('userId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('userInternalId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('providerType'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('providerId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('providerInternalId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('identifier'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('name'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => ID::custom('_key_userId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['userId'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC],
+ ],
+ [
+ '$id' => ID::custom('_key_userInternalId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['userInternalId'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC],
+ ],
+ [
+ '$id' => ID::custom('_key_providerId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['providerId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_providerInternalId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['providerInternalId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_identifier'),
+ 'type' => Database::INDEX_UNIQUE,
+ 'attributes' => ['identifier'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ ],
+ ],
+];
$projectCollections = array_merge([
'databases' => [
@@ -3412,7 +4024,7 @@ $consoleCollections = array_merge([
'filters' => ['json'],
],
[
- '$id' => ID::custom('authProviders'),
+ '$id' => ID::custom('oAuthProviders'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
diff --git a/app/config/errors.php b/app/config/errors.php
index c0628920d9..2e35bfb881 100644
--- a/app/config/errors.php
+++ b/app/config/errors.php
@@ -103,6 +103,16 @@ return [
'description' => 'This method was not fully implemented yet. If you believe this is a mistake, please upgrade your Appwrite server version.',
'code' => 405,
],
+ Exception::GENERAL_INVALID_EMAIL => [
+ 'name' => Exception::GENERAL_INVALID_EMAIL,
+ 'description' => 'Value must be a valid email address.',
+ 'code' => 400,
+ ],
+ Exception::GENERAL_INVALID_PHONE => [
+ 'name' => Exception::GENERAL_INVALID_PHONE,
+ 'description' => 'Value must be a valid phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.',
+ 'code' => 400,
+ ],
/** User Errors */
Exception::USER_COUNT_EXCEEDED => [
@@ -251,6 +261,16 @@ return [
'description' => 'User phone is already verified',
'code' => 409
],
+ Exception::USER_TARGET_NOT_FOUND => [
+ 'name' => Exception::USER_TARGET_NOT_FOUND,
+ 'description' => 'The target could not be found.',
+ 'code' => 404,
+ ],
+ Exception::USER_TARGET_ALREADY_EXISTS => [
+ 'name' => Exception::USER_TARGET_ALREADY_EXISTS,
+ 'description' => 'A target with the same ID already exists.',
+ 'code' => 409,
+ ],
/** Teams */
Exception::TEAM_NOT_FOUND => [
@@ -773,4 +793,83 @@ return [
'code' => 503,
'publish' => false
],
+
+ /** Providers */
+ Exception::PROVIDER_NOT_FOUND => [
+ 'name' => Exception::PROVIDER_NOT_FOUND,
+ 'description' => 'Provider with the requested ID could not be found.',
+ 'code' => 404,
+ ],
+ Exception::PROVIDER_ALREADY_EXISTS => [
+ 'name' => Exception::PROVIDER_ALREADY_EXISTS,
+ 'description' => 'Provider with the requested ID already exists.',
+ 'code' => 409,
+ ],
+ Exception::PROVIDER_INCORRECT_TYPE => [
+ 'name' => Exception::PROVIDER_INCORRECT_TYPE,
+ 'description' => 'Provider with the requested ID is of incorrect type: ',
+ 'code' => 400,
+ ],
+
+ /** Topics */
+ Exception::TOPIC_NOT_FOUND => [
+ 'name' => Exception::TOPIC_NOT_FOUND,
+ 'description' => 'Topic with the request ID could not be found.',
+ 'code' => 404,
+ ],
+ Exception::TOPIC_ALREADY_EXISTS => [
+ 'name' => Exception::TOPIC_ALREADY_EXISTS,
+ 'description' => 'Topic with the request ID already exists.',
+ 'code' => 409,
+ ],
+
+ /** Subscribers */
+ Exception::SUBSCRIBER_NOT_FOUND => [
+ 'name' => Exception::SUBSCRIBER_NOT_FOUND,
+ 'description' => 'Subscriber with the request ID could not be found.',
+ 'code' => 404,
+ ],
+ Exception::SUBSCRIBER_ALREADY_EXISTS => [
+ 'name' => Exception::SUBSCRIBER_ALREADY_EXISTS,
+ 'description' => 'Subscriber with the request ID already exists.',
+ 'code' => 409,
+ ],
+
+ /** Messages */
+ Exception::MESSAGE_NOT_FOUND => [
+ 'name' => Exception::MESSAGE_NOT_FOUND,
+ 'description' => 'Message with the requested ID could not be found.',
+ 'code' => 404,
+ ],
+ Exception::MESSAGE_MISSING_TARGET => [
+ 'name' => Exception::MESSAGE_MISSING_TARGET,
+ 'description' => 'Message with the requested ID is missing a target (Topics or Users or Targets).',
+ 'code' => 400,
+ ],
+ Exception::MESSAGE_ALREADY_SENT => [
+ 'name' => Exception::MESSAGE_ALREADY_SENT,
+ 'description' => 'Message with the requested ID has already been sent.',
+ 'code' => 400,
+ ],
+ Exception::MESSAGE_ALREADY_SCHEDULED => [
+ 'name' => Exception::MESSAGE_ALREADY_SCHEDULED,
+ 'description' => 'Message with the requested ID has already been scheduled for delivery.',
+ 'code' => 400,
+ ],
+ Exception::MESSAGE_TARGET_NOT_EMAIL => [
+ 'name' => Exception::MESSAGE_TARGET_NOT_EMAIL,
+ 'description' => 'Message with the target ID is not an email target:',
+ 'code' => 400,
+ ],
+ Exception::MESSAGE_TARGET_NOT_SMS => [
+ 'name' => Exception::MESSAGE_TARGET_NOT_SMS,
+ 'description' => 'Message with the target ID is not an SMS target:',
+ 'code' => 400,
+ ],
+ Exception::MESSAGE_TARGET_NOT_PUSH => [
+ 'name' => Exception::MESSAGE_TARGET_NOT_PUSH,
+ 'description' => 'Message with the target ID is not a push target:',
+ 'code' => 400,
+ ],
+
];
diff --git a/app/config/events.php b/app/config/events.php
index c0b6cc3b41..b0db9090fb 100644
--- a/app/config/events.php
+++ b/app/config/events.php
@@ -44,6 +44,20 @@ return [
'$description' => 'This event triggers when a verification token for a user is validated.'
],
],
+ 'targets' => [
+ '$model' => Response::MODEL_TARGET,
+ '$resource' => true,
+ '$description' => 'This event triggers on any user\'s target event.',
+ 'create' => [
+ '$description' => 'This event triggers when a user\'s target is created.',
+ ],
+ 'update' => [
+ '$description' => 'This event triggers when a user\'s target is updated.',
+ ],
+ 'delete' => [
+ '$description' => 'This event triggers when a user\'s target is deleted.',
+ ],
+ ],
'create' => [
'$description' => 'This event triggers when a user is created.'
],
@@ -237,6 +251,56 @@ return [
'$description' => 'This event triggers when a function is updated.',
]
],
+ 'messages' => [
+ '$model' => Response::MODEL_MESSAGE,
+ '$resource' => true,
+ '$description' => 'This event triggers on any messaging event.',
+ 'create' => [
+ '$description' => 'This event triggers when a message is created.',
+ ],
+ 'update' => [
+ '$description' => 'This event triggers when a message is updated.',
+ ],
+ ],
+ 'topics' => [
+ '$model' => Response::MODEL_TOPIC,
+ '$resource' => true,
+ '$description' => 'This event triggers on any topic event.',
+ 'create' => [
+ '$description' => 'This event triggers when a topic is created.',
+ ],
+ 'update' => [
+ '$description' => 'This event triggers when a topic is updated.',
+ ],
+ 'delete' => [
+ '$description' => 'This event triggers when a topic is deleted.'
+ ],
+ 'subscribers' => [
+ '$model' => Response::MODEL_SUBSCRIBER,
+ '$resource' => true,
+ '$description' => 'This event triggers on any subscriber event.',
+ 'create' => [
+ '$description' => 'This event triggers when a subscriber is created.',
+ ],
+ 'delete' => [
+ '$description' => 'This event triggers when a subscriber is deleted.'
+ ],
+ ],
+ ],
+ 'providers' => [
+ '$model' => Response::MODEL_PROVIDER,
+ '$resource' => true,
+ '$description' => 'This event triggers on any provider event.',
+ 'create' => [
+ '$description' => 'This event triggers when a provider is created.',
+ ],
+ 'update' => [
+ '$description' => 'This event triggers when a provider is updated.',
+ ],
+ 'delete' => [
+ '$description' => 'This event triggers when a provider is deleted.'
+ ],
+ ],
'rules' => [
'$model' => Response::MODEL_PROXY_RULE,
'$resource' => true,
diff --git a/app/config/roles.php b/app/config/roles.php
index 65d5694bbf..944fcf3577 100644
--- a/app/config/roles.php
+++ b/app/config/roles.php
@@ -21,6 +21,10 @@ $member = [
'avatars.read',
'execution.read',
'execution.write',
+ 'targets.read',
+ 'targets.write',
+ 'subscribers.write',
+ 'subscribers.read',
'assistant.read',
];
@@ -60,6 +64,16 @@ $admins = [
'migrations.write',
'vcs.read',
'vcs.write',
+ 'targets.read',
+ 'targets.write',
+ 'providers.write',
+ 'providers.read',
+ 'messages.write',
+ 'messages.read',
+ 'topics.write',
+ 'topics.read',
+ 'subscribers.write',
+ 'subscribers.read'
];
return [
diff --git a/app/config/scopes.php b/app/config/scopes.php
index acafc7623c..ddc1de21f3 100644
--- a/app/config/scopes.php
+++ b/app/config/scopes.php
@@ -76,6 +76,36 @@ return [ // List of publicly visible scopes
'health.read' => [
'description' => 'Access to read your project\'s health status',
],
+ 'providers.read' => [
+ 'description' => 'Access to read your project\'s providers',
+ ],
+ 'providers.write' => [
+ 'description' => 'Access to create, update, and delete your project\'s providers',
+ ],
+ 'messages.read' => [
+ 'description' => 'Access to read your project\'s messages',
+ ],
+ 'messages.write' => [
+ 'description' => 'Access to create, update, and delete your project\'s messages',
+ ],
+ 'topics.read' => [
+ 'description' => 'Access to read your project\'s topics',
+ ],
+ 'topics.write' => [
+ 'description' => 'Access to create, update, and delete your project\'s topics',
+ ],
+ 'subscribers.read' => [
+ 'description' => 'Access to read your project\'s subscribers',
+ ],
+ 'subscribers.write' => [
+ 'description' => 'Access to create, update, and delete your project\'s subscribers',
+ ],
+ 'targets.read' => [
+ 'description' => 'Access to read your project\'s targets',
+ ],
+ 'targets.write' => [
+ 'description' => 'Access to create, update, and delete your project\'s targets',
+ ],
'rules.read' => [
'description' => 'Access to read your project\'s proxy rules',
],
diff --git a/app/config/services.php b/app/config/services.php
index 5c2233dfc6..c4fb98c59a 100644
--- a/app/config/services.php
+++ b/app/config/services.php
@@ -251,4 +251,17 @@ return [
'optional' => false,
'icon' => '/images/services/migrations.png',
],
+ 'messaging' => [
+ 'key' => 'messaging',
+ 'name' => 'Messaging',
+ 'subtitle' => 'The Messaging service allows you to send messages to any provider type (SMTP, push notification, SMS, etc.).',
+ 'description' => '/docs/services/messaging.md',
+ 'controller' => 'api/messaging.php',
+ 'sdk' => true,
+ 'docs' => true,
+ 'docsUrl' => 'https://appwrite.io/docs/server/messaging',
+ 'tests' => true,
+ 'optional' => true,
+ 'icon' => '/images/services/messaging.png',
+ ]
];
diff --git a/app/config/variables.php b/app/config/variables.php
index 9d555bf013..c9329f6d55 100644
--- a/app/config/variables.php
+++ b/app/config/variables.php
@@ -440,7 +440,7 @@ return [
'variables' => [
[
'name' => '_APP_SMS_PROVIDER',
- 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, text-magic, telesign, msg91, and vonage.",
+ 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, Textmagic, telesign, msg91, and vonage.",
'introduction' => '0.15.0',
'default' => '',
'required' => false,
diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php
index 54967fb502..b85bdc0133 100644
--- a/app/controllers/api/account.php
+++ b/app/controllers/api/account.php
@@ -8,7 +8,6 @@ use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
-use Appwrite\Event\Phone as EventPhone;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
@@ -45,6 +44,7 @@ use Utopia\Validator\WhiteList;
use Appwrite\Auth\Validator\PasswordHistory;
use Appwrite\Auth\Validator\PasswordDictionary;
use Appwrite\Auth\Validator\PersonalData;
+use Appwrite\Event\Messaging;
$oauthDefaultSuccess = '/auth/oauth2/success';
$oauthDefaultFailure = '/auth/oauth2/failure';
@@ -69,7 +69,7 @@ App::post('/v1/account')
->label('abuse-limit', 10)
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
- ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
+ ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('request')
->inject('response')
@@ -148,7 +148,22 @@ App::post('/v1/account')
'accessedAt' => DateTime::now(),
]);
$user->removeAttribute('$internalId');
- Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
+ $user = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
+ try {
+ $target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
+ 'userId' => $user->getId(),
+ 'userInternalId' => $user->getInternalId(),
+ 'providerType' => MESSAGE_TYPE_EMAIL,
+ 'identifier' => $email,
+ ])));
+ $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
+ } catch (Duplicate) {
+ $existingTarget = $dbForProject->findOne('targets', [
+ Query::equal('identifier', [$email]),
+ ]);
+ $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
+ }
+ $dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@@ -299,7 +314,7 @@ App::get('/v1/account/sessions/oauth2/:provider')
->label('sdk.methodType', 'webAuth')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
- ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('providers'), fn($node) => (!$node['mock'])))) . '.')
+ ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn($node) => (!$node['mock'])))) . '.')
->param('success', '', fn($clients) => new Host($clients), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
->param('failure', '', fn($clients) => new Host($clients), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
@@ -311,14 +326,14 @@ App::get('/v1/account/sessions/oauth2/:provider')
$protocol = $request->getProtocol();
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
- $providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false;
+ $providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
if (!$providerEnabled) {
throw new Exception(Exception::PROJECT_PROVIDER_DISABLED, 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.');
}
- $appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
- $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
+ $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
+ $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
if (!empty($appSecret) && isset($appSecret['version'])) {
$key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']);
@@ -358,7 +373,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('scope', 'public')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
- ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
+ ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->param('state', '', new Text(2048), 'Login state params.', true)
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
@@ -391,7 +406,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('origin', '*')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
- ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
+ ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->param('state', '', new Text(2048), 'Login state params.', true)
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
@@ -430,7 +445,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->label('docs', false)
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('usage.params', ['provider:{request.provider}'])
- ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
+ ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->param('state', '', new Text(2048), 'OAuth2 state params.', true)
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
@@ -448,9 +463,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => ''];
$validateURL = new URL();
- $appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
- $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
- $providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false;
+ $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
+ $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
+ $providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
@@ -458,7 +473,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
- $providers = Config::getParam('providers');
+ $providers = Config::getParam('oAuthProviders');
$providerName = $providers[$provider]['name'] ?? '';
/** @var Appwrite\Auth\OAuth2 $oauth2 */
@@ -664,7 +679,18 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'accessedAt' => DateTime::now(),
]);
$user->removeAttribute('$internalId');
- Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
+ $userDoc = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
+ $dbForProject->createDocument('targets', new Document([
+ '$permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::user($user->getId())),
+ Permission::delete(Role::user($user->getId())),
+ ],
+ 'userId' => $userDoc->getId(),
+ 'userInternalId' => $userDoc->getInternalId(),
+ 'providerType' => MESSAGE_TYPE_EMAIL,
+ 'identifier' => $email,
+ ]));
} catch (Duplicate) {
$failureRedirect(Exception::USER_ALREADY_EXISTS);
}
@@ -1243,8 +1269,7 @@ App::post('/v1/account/sessions/phone')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('locale')
- ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, EventPhone $queueForMessaging, Locale $locale) {
-
+ ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -1294,6 +1319,21 @@ App::post('/v1/account/sessions/phone')
$user->removeAttribute('$internalId');
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
+ try {
+ $target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
+ 'userId' => $user->getId(),
+ 'userInternalId' => $user->getInternalId(),
+ 'providerType' => MESSAGE_TYPE_SMS,
+ 'identifier' => $phone,
+ ])));
+ $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
+ } catch (Duplicate) {
+ $existingTarget = $dbForProject->findOne('targets', [
+ Query::equal('identifier', [$phone]),
+ ]);
+ $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
+ }
+ $dbForProject->deleteCachedDocument('users', $user->getId());
}
$secret = Auth::codeGenerator();
@@ -1331,9 +1371,19 @@ App::post('/v1/account/sessions/phone')
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
+
+ $messageDoc = new Document([
+ '$id' => $token->getId(),
+ 'data' => [
+ 'content' => $message,
+ ],
+ ]);
+
$queueForMessaging
- ->setRecipient($phone)
- ->setMessage($message)
+ ->setMessage($messageDoc)
+ ->setRecipients([$phone])
+ ->setProviderType(MESSAGE_TYPE_SMS)
+ ->setProject($project)
->trigger();
$queueForEvents->setPayload(
@@ -1652,6 +1702,81 @@ App::post('/v1/account/jwt')
])]), Response::MODEL_JWT);
});
+App::post('/v1/account/targets/push')
+ ->desc('Create Account\'s push target')
+ ->groups(['api', 'account'])
+ ->label('error', __DIR__ . '/../../views/general/error.phtml')
+ ->label('audits.event', 'target.create')
+ ->label('audits.resource', 'target/response.$id')
+ ->label('event', 'users.[userId].targets.[targetId].create')
+ ->label('sdk.auth', [APP_AUTH_TYPE_SESSION])
+ ->label('sdk.namespace', 'account')
+ ->label('sdk.method', 'createPushTarget')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_TARGET)
+ ->label('docs', false)
+ ->param('targetId', '', new CustomId(), 'Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.')
+ ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
+ ->inject('queueForEvents')
+ ->inject('user')
+ ->inject('request')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->action(function (string $targetId, string $providerId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) {
+ $targetId = $targetId == 'unique()' ? ID::unique() : $targetId;
+
+ $provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId));
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+
+ if ($user->isEmpty()) {
+ throw new Exception(Exception::USER_NOT_FOUND);
+ }
+
+ $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
+
+ if (!$target->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
+ }
+
+ $detector = new Detector($request->getUserAgent());
+ $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
+
+ $device = $detector->getDevice();
+
+ try {
+ $target = $dbForProject->createDocument('targets', new Document([
+ '$id' => $targetId,
+ '$permissions' => [
+ Permission::read(Role::user($user->getId())),
+ Permission::update(Role::user($user->getId())),
+ ],
+ 'providerId' => $providerId ?? null,
+ 'providerInternalId' => $provider->getInternalId() ?? null,
+ 'providerType' => MESSAGE_TYPE_PUSH,
+ 'userId' => $user->getId(),
+ 'userInternalId' => $user->getInternalId(),
+ 'identifier' => $identifier,
+ 'name' => "{$device['deviceBrand']} {$device['deviceModel']}"
+ ]));
+ } catch (Duplicate) {
+ throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
+ }
+ $dbForProject->deleteCachedDocument('users', $user->getId());
+
+ $queueForEvents
+ ->setParam('userId', $user->getId())
+ ->setParam('targetId', $target->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($target, Response::MODEL_TARGET);
+ });
+
App::get('/v1/account')
->desc('Get account')
->groups(['api', 'account'])
@@ -1977,6 +2102,7 @@ App::patch('/v1/account/email')
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
+ $oldEmail = $user->getAttribute('email');
$email = \strtolower($email);
// Makes sure this email is not already used in another identity
@@ -2001,8 +2127,25 @@ App::patch('/v1/account/email')
->setAttribute('passwordUpdate', DateTime::now());
}
+ $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [
+ Query::equal('identifier', [$email]),
+ ]));
+
+ if ($target instanceof Document && !$target->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
+ }
+
try {
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
+ /**
+ * @var Document $oldTarget
+ */
+ $oldTarget = $user->find('identifier', $oldEmail, 'targets');
+
+ if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
+ Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)));
+ }
+ $dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@@ -2047,6 +2190,16 @@ App::patch('/v1/account/phone')
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
+ $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [
+ Query::equal('identifier', [$phone]),
+ ]));
+
+ if ($target instanceof Document && !$target->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
+ }
+
+ $oldPhone = $user->getAttribute('phone');
+
$user
->setAttribute('phone', $phone)
->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again
@@ -2062,6 +2215,15 @@ App::patch('/v1/account/phone')
try {
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
+ /**
+ * @var Document $oldTarget
+ */
+ $oldTarget = $user->find('identifier', $oldPhone, 'targets');
+
+ if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
+ Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone)));
+ }
+ $dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS);
}
@@ -2272,8 +2434,8 @@ App::patch('/v1/account/sessions/:sessionId')
$provider = $session->getAttribute('provider');
$refreshToken = $session->getAttribute('providerRefreshToken');
- $appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
- $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
+ $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
+ $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
@@ -2570,8 +2732,8 @@ App::put('/v1/account/recovery')
->label('abuse-key', 'url:{url},userId:{param-userId}')
->param('userId', '', new UID(), 'User ID.')
->param('secret', '', new Text(256), 'Valid reset token.')
- ->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
- ->param('passwordAgain', '', new Password(), 'Repeat new user password. Must be at least 8 chars.')
+ ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
+ ->param('passwordAgain', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Repeat new user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
->inject('response')
->inject('user')
->inject('dbForProject')
@@ -2885,10 +3047,9 @@ App::post('/v1/account/verification/phone')
->inject('queueForMessaging')
->inject('project')
->inject('locale')
- ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, EventPhone $queueForMessaging, Document $project, Locale $locale) {
-
+ ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
- throw new Exception(Exception::GENERAL_PHONE_DISABLED);
+ throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
if (empty($user->getAttribute('phone'))) {
@@ -2937,11 +3098,19 @@ App::post('/v1/account/verification/phone')
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
+ $messageDoc = new Document([
+ '$id' => $verification->getId(),
+ 'data' => [
+ 'content' => $message,
+ ],
+ ]);
+
$queueForMessaging
- ->setRecipient($user->getAttribute('phone'))
- ->setMessage($message)
- ->trigger()
- ;
+ ->setMessage($messageDoc)
+ ->setRecipients([$user->getAttribute('phone')])
+ ->setProviderType(MESSAGE_TYPE_SMS)
+ ->setProject($project)
+ ->trigger();
$queueForEvents
->setParam('userId', $user->getId())
@@ -3018,3 +3187,61 @@ App::put('/v1/account/verification/phone')
$response->dynamic($verificationDocument, Response::MODEL_TOKEN);
});
+
+App::put('/v1/account/targets/:targetId/push')
+ ->desc('Update Account\'s push target')
+ ->groups(['api', 'account'])
+ ->label('error', __DIR__ . '/../../views/general/error.phtml')
+ ->label('audits.event', 'target.update')
+ ->label('audits.resource', 'target/response.$id')
+ ->label('event', 'users.[userId].targets.[targetId].update')
+ ->label('sdk.auth', [APP_AUTH_TYPE_SESSION])
+ ->label('sdk.namespace', 'account')
+ ->label('sdk.method', 'updatePushTarget')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_TARGET)
+ ->label('docs', false)
+ ->param('targetId', '', new UID(), 'Target ID.')
+ ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
+ ->inject('queueForEvents')
+ ->inject('user')
+ ->inject('request')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) {
+ if ($user->isEmpty()) {
+ throw new Exception(Exception::USER_NOT_FOUND);
+ }
+
+ $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
+
+ if ($target->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($user->getId() !== $target->getAttribute('userId')) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($identifier) {
+ $target->setAttribute('identifier', $identifier);
+ }
+
+ $detector = new Detector($request->getUserAgent());
+ $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
+
+ $device = $detector->getDevice();
+
+ $target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}");
+
+ $target = $dbForProject->updateDocument('targets', $target->getId(), $target);
+ $dbForProject->deleteCachedDocument('users', $user->getId());
+
+ $queueForEvents
+ ->setParam('userId', $user->getId())
+ ->setParam('targetId', $target->getId());
+
+ $response
+ ->dynamic($target, Response::MODEL_TARGET);
+ });
diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php
index e0d967eb00..b6395774e9 100644
--- a/app/controllers/api/avatars.php
+++ b/app/controllers/api/avatars.php
@@ -84,8 +84,8 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
$accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry');
$refreshToken = $gitHubSession->getAttribute('providerRefreshToken');
- $appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
- $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
+ $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
+ $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php
index 0d2e2f22d1..b3104f6736 100644
--- a/app/controllers/api/databases.php
+++ b/app/controllers/api/databases.php
@@ -1561,7 +1561,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
- ->param('default', null, new DatetimeValidator(), 'Default value for the attribute in ISO 8601 format. Cannot be set when attribute is required.', true)
+ ->param('default', null, new DatetimeValidator(), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php
new file mode 100644
index 0000000000..124bc2d219
--- /dev/null
+++ b/app/controllers/api/messaging.php
@@ -0,0 +1,2961 @@
+desc('Create Mailgun provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.create')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].create')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createMailgunProvider')
+ ->label('sdk.description', '/docs/references/messaging/create-mailgun-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('name', '', new Text(128), 'Provider name.')
+ ->param('from', '', new Email(), 'Sender email address.', true)
+ ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
+ ->param('domain', '', new Text(0), 'Mailgun Domain.', true)
+ ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, string $from, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+
+ $options = [];
+
+ if (!empty($from)) {
+ $options ['from'] = $from;
+ }
+
+ $credentials = [];
+
+ if ($isEuRegion === true || $isEuRegion === false) {
+ $credentials['isEuRegion'] = $isEuRegion;
+ }
+
+ if (!empty($apiKey)) {
+ $credentials['apiKey'] = $apiKey;
+ }
+
+ if (!empty($domain)) {
+ $credentials['domain'] = $domain;
+ }
+
+ if (
+ $enabled === true &&
+ \array_key_exists('isEuRegion', $credentials) &&
+ \array_key_exists('apiKey', $credentials) &&
+ \array_key_exists('domain', $credentials) &&
+ \array_key_exists('from', $options)
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'mailgun',
+ 'type' => MESSAGE_TYPE_EMAIL,
+ 'enabled' => $enabled,
+ 'credentials' => $credentials,
+ 'options' => $options,
+ ]);
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::post('/v1/messaging/providers/sendgrid')
+ ->desc('Create Sendgrid provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.create')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].create')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createSendgridProvider')
+ ->label('sdk.description', '/docs/references/messaging/create-sengrid-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('name', '', new Text(128), 'Provider name.')
+ ->param('from', '', new Email(), 'Sender email address.', true)
+ ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, string $from, string $apiKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+
+ $options = [];
+
+ if (!empty($from)) {
+ $options ['from'] = $from;
+ }
+
+ $credentials = [];
+
+ if (!empty($apiKey)) {
+ $credentials['apiKey'] = $apiKey;
+ }
+
+ if (
+ $enabled === true
+ && \array_key_exists('apiKey', $credentials)
+ && \array_key_exists('from', $options)
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'sendgrid',
+ 'type' => MESSAGE_TYPE_EMAIL,
+ 'enabled' => $enabled,
+ 'credentials' => $credentials,
+ 'options' => $options,
+ ]);
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::post('/v1/messaging/providers/msg91')
+ ->desc('Create Msg91 provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.create')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('scope', 'providers.write')
+ ->label('event', 'providers.[providerId].create')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createMsg91Provider')
+ ->label('sdk.description', '/docs/references/messaging/create-msg91-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('name', '', new Text(128), 'Provider name.')
+ ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
+ ->param('senderId', '', new Text(0), 'Msg91 Sender ID.', true)
+ ->param('authKey', '', new Text(0), 'Msg91 Auth Key.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, string $from, string $senderId, string $authKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+
+ $options = [];
+
+ if (!empty($from)) {
+ $options ['from'] = $from;
+ }
+
+ $credentials = [];
+
+ if (!empty($senderId)) {
+ $credentials['senderId'] = $senderId;
+ }
+
+ if (!empty($authKey)) {
+ $credentials['authKey'] = $authKey;
+ }
+
+ if (
+ $enabled === true
+ && \array_key_exists('senderId', $credentials)
+ && \array_key_exists('authKey', $credentials)
+ && \array_key_exists('from', $options)
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'msg91',
+ 'type' => MESSAGE_TYPE_SMS,
+ 'enabled' => $enabled,
+ 'credentials' => $credentials,
+ 'options' => $options,
+ ]);
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::post('/v1/messaging/providers/telesign')
+ ->desc('Create Telesign provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.create')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].create')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createTelesignProvider')
+ ->label('sdk.description', '/docs/references/messaging/create-telesign-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('name', '', new Text(128), 'Provider name.')
+ ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
+ ->param('username', '', new Text(0), 'Telesign username.', true)
+ ->param('password', '', new Text(0), 'Telesign password.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, string $from, string $username, string $password, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+
+ $options = [];
+
+ if (!empty($from)) {
+ $options ['from'] = $from;
+ }
+
+ $credentials = [];
+
+ if (!empty($username)) {
+ $credentials['username'] = $username;
+ }
+
+ if (!empty($password)) {
+ $credentials['password'] = $password;
+ }
+
+ if (
+ $enabled === true
+ && \array_key_exists('username', $credentials)
+ && \array_key_exists('password', $credentials)
+ && \array_key_exists('from', $options)
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'telesign',
+ 'type' => MESSAGE_TYPE_SMS,
+ 'enabled' => $enabled,
+ 'credentials' => $credentials,
+ 'options' => $options,
+ ]);
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::post('/v1/messaging/providers/textmagic')
+ ->desc('Create Textmagic provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.create')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].create')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createTextmagicProvider')
+ ->label('sdk.description', '/docs/references/messaging/create-textmagic-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('name', '', new Text(128), 'Provider name.')
+ ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
+ ->param('username', '', new Text(0), 'Textmagic username.', true)
+ ->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, string $from, string $username, string $apiKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+
+ $options = [];
+
+ if (!empty($from)) {
+ $options ['from'] = $from;
+ }
+
+ $credentials = [];
+
+ if (!empty($username)) {
+ $credentials['username'] = $username;
+ }
+
+ if (!empty($apiKey)) {
+ $credentials['apiKey'] = $apiKey;
+ }
+
+ if (
+ $enabled === true
+ && \array_key_exists('username', $credentials)
+ && \array_key_exists('apiKey', $credentials)
+ && \array_key_exists('from', $options)
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'textmagic',
+ 'type' => MESSAGE_TYPE_SMS,
+ 'enabled' => $enabled,
+ 'credentials' => $credentials,
+ 'options' => $options,
+ ]);
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::post('/v1/messaging/providers/twilio')
+ ->desc('Create Twilio provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.create')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].create')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createTwilioProvider')
+ ->label('sdk.description', '/docs/references/messaging/create-twilio-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('name', '', new Text(128), 'Provider name.')
+ ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
+ ->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true)
+ ->param('authToken', '', new Text(0), 'Twilio authentication token.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, string $from, string $accountSid, string $authToken, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+
+ $options = [];
+
+ if (!empty($from)) {
+ $options ['from'] = $from;
+ }
+
+ $credentials = [];
+
+ if (!empty($accountSid)) {
+ $credentials['accountSid'] = $accountSid;
+ }
+
+ if (!empty($authToken)) {
+ $credentials['authToken'] = $authToken;
+ }
+
+ if (
+ $enabled === true
+ && \array_key_exists('accountSid', $credentials)
+ && \array_key_exists('authToken', $credentials)
+ && \array_key_exists('from', $options)
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'twilio',
+ 'type' => MESSAGE_TYPE_SMS,
+ 'enabled' => $enabled,
+ 'credentials' => $credentials,
+ 'options' => $options,
+ ]);
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::post('/v1/messaging/providers/vonage')
+ ->desc('Create Vonage provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.create')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].create')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createVonageProvider')
+ ->label('sdk.description', '/docs/references/messaging/create-vonage-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('name', '', new Text(128), 'Provider name.')
+ ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
+ ->param('apiKey', '', new Text(0), 'Vonage API key.', true)
+ ->param('apiSecret', '', new Text(0), 'Vonage API secret.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, string $from, string $apiKey, string $apiSecret, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+
+ $options = [];
+
+ if (!empty($from)) {
+ $options ['from'] = $from;
+ }
+
+ $credentials = [];
+
+ if (!empty($apiKey)) {
+ $credentials['apiKey'] = $apiKey;
+ }
+
+ if (!empty($apiSecret)) {
+ $credentials['apiSecret'] = $apiSecret;
+ }
+
+ if (
+ $enabled === true
+ && \array_key_exists('apiKey', $credentials)
+ && \array_key_exists('apiSecret', $credentials)
+ && \array_key_exists('from', $options)
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'vonage',
+ 'type' => MESSAGE_TYPE_SMS,
+ 'enabled' => $enabled,
+ 'credentials' => $credentials,
+ 'options' => $options,
+ ]);
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::post('/v1/messaging/providers/fcm')
+ ->desc('Create FCM provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.create')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].create')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createFcmProvider')
+ ->label('sdk.description', '/docs/references/messaging/create-fcm-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('name', '', new Text(128), 'Provider name.')
+ ->param('serverKey', '', new Text(0), 'FCM server key.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, string $serverKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+
+ $credentials = [];
+
+ if (!empty($serverKey)) {
+ $credentials['serverKey'] = $serverKey;
+ }
+
+ if ($enabled === true && \array_key_exists('serverKey', $credentials)) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'fcm',
+ 'type' => MESSAGE_TYPE_PUSH,
+ 'enabled' => $enabled,
+ 'credentials' => $credentials
+ ]);
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::post('/v1/messaging/providers/apns')
+ ->desc('Create APNS provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.create')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].create')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createApnsProvider')
+ ->label('sdk.description', '/docs/references/messaging/create-apns-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('name', '', new Text(128), 'Provider name.')
+ ->param('authKey', '', new Text(0), 'APNS authentication key.', true)
+ ->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true)
+ ->param('teamId', '', new Text(0), 'APNS team ID.', true)
+ ->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
+ ->param('endpoint', '', new Text(0), 'APNS endpoint.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+
+ $credentials = [];
+
+ if (!empty($authKey)) {
+ $credentials['authKey'] = $authKey;
+ }
+
+ if (!empty($authKeyId)) {
+ $credentials['authKeyId'] = $authKeyId;
+ }
+
+ if (!empty($teamId)) {
+ $credentials['teamId'] = $teamId;
+ }
+
+ if (!empty($bundleId)) {
+ $credentials['bundleId'] = $bundleId;
+ }
+
+ if (!empty($endpoint)) {
+ $credentials['endpoint'] = $endpoint;
+ }
+
+ if (
+ $enabled === true
+ && \array_key_exists('authKey', $credentials)
+ && \array_key_exists('authKeyId', $credentials)
+ && \array_key_exists('teamId', $credentials)
+ && \array_key_exists('bundleId', $credentials)
+ && \array_key_exists('endpoint', $credentials)
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'apns',
+ 'type' => MESSAGE_TYPE_PUSH,
+ 'enabled' => $enabled,
+ 'credentials' => $credentials,
+ ]);
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::get('/v1/messaging/providers')
+ ->desc('List providers')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'providers.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'listProviders')
+ ->label('sdk.description', '/docs/references/messaging/list-providers.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER_LIST)
+ ->param('queries', [], new Providers(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Providers::ALLOWED_ATTRIBUTES), true)
+ ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
+ $queries = Query::parseQueries($queries);
+
+ if (!empty($search)) {
+ $queries[] = Query::search('search', $search);
+ }
+
+ // Get cursor document if there was a cursor query
+ $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
+ $cursor = reset($cursor);
+
+ if ($cursor) {
+ $providerId = $cursor->getValue();
+ $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId));
+
+ if ($cursorDocument->isEmpty()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $response->dynamic(new Document([
+ 'providers' => $dbForProject->find('providers', $queries),
+ 'total' => $dbForProject->count('providers', $queries, APP_LIMIT_COUNT),
+ ]), Response::MODEL_PROVIDER_LIST);
+ });
+
+App::get('/v1/messaging/providers/:providerId/logs')
+ ->desc('List provider logs')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'providers.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'listProviderLogs')
+ ->label('sdk.description', '/docs/references/messaging/providers/get-logs.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_LOG_LIST)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('locale')
+ ->inject('geodb')
+ ->action(function (string $providerId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+
+ $queries = Query::parseQueries($queries);
+ $grouped = Query::groupByType($queries);
+ $limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
+ $offset = $grouped['offset'] ?? 0;
+
+ $audit = new Audit($dbForProject);
+ $resource = 'provider/' . $providerId;
+ $logs = $audit->getLogsByResource($resource, $limit, $offset);
+ $output = [];
+
+ foreach ($logs as $i => &$log) {
+ $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
+
+ $detector = new Detector($log['userAgent']);
+ $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
+
+ $os = $detector->getOS();
+ $client = $detector->getClient();
+ $device = $detector->getDevice();
+
+ $output[$i] = new Document([
+ 'event' => $log['event'],
+ 'userId' => ID::custom($log['data']['userId']),
+ 'userEmail' => $log['data']['userEmail'] ?? null,
+ 'userName' => $log['data']['userName'] ?? null,
+ 'mode' => $log['data']['mode'] ?? null,
+ 'ip' => $log['ip'],
+ 'time' => $log['time'],
+ 'osCode' => $os['osCode'],
+ 'osName' => $os['osName'],
+ 'osVersion' => $os['osVersion'],
+ 'clientType' => $client['clientType'],
+ 'clientCode' => $client['clientCode'],
+ 'clientName' => $client['clientName'],
+ 'clientVersion' => $client['clientVersion'],
+ 'clientEngine' => $client['clientEngine'],
+ 'clientEngineVersion' => $client['clientEngineVersion'],
+ 'deviceName' => $device['deviceName'],
+ 'deviceBrand' => $device['deviceBrand'],
+ 'deviceModel' => $device['deviceModel']
+ ]);
+
+ $record = $geodb->get($log['ip']);
+
+ if ($record) {
+ $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
+ $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
+ } else {
+ $output[$i]['countryCode'] = '--';
+ $output[$i]['countryName'] = $locale->getText('locale.country.unknown');
+ }
+ }
+
+ $response->dynamic(new Document([
+ 'total' => $audit->countLogsByResource($resource),
+ 'logs' => $output,
+ ]), Response::MODEL_LOG_LIST);
+ });
+
+App::get('/v1/messaging/providers/:providerId')
+ ->desc('Get provider')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'providers.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'getProvider')
+ ->label('sdk.description', '/docs/references/messaging/get-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+
+ $response->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/mailgun/:providerId')
+ ->desc('Update Mailgun provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.update')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].update')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateMailgunProvider')
+ ->label('sdk.description', '/docs/references/messaging/update-mailgun-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('name', '', new Text(128), 'Provider name.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
+ ->param('from', '', new Email(), 'Sender email address.', true)
+ ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
+ ->param('domain', '', new Text(0), 'Mailgun Domain.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $from, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+ $providerAttr = $provider->getAttribute('provider');
+
+ if ($providerAttr !== 'mailgun') {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ if (!empty($name)) {
+ $provider->setAttribute('name', $name);
+ }
+
+ if (!empty($from)) {
+ $provider->setAttribute('options', [
+ 'from' => $from,
+ ]);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if ($isEuRegion === true || $isEuRegion === false) {
+ $credentials['isEuRegion'] = $isEuRegion;
+ }
+
+ if (!empty($apiKey)) {
+ $credentials['apiKey'] = $apiKey;
+ }
+
+ if (!empty($domain)) {
+ $credentials['domain'] = $domain;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ if ($enabled === true || $enabled === false) {
+ if (
+ $enabled === true &&
+ \array_key_exists('isEuRegion', $credentials) &&
+ \array_key_exists('apiKey', $credentials) &&
+ \array_key_exists('domain', $credentials) &&
+ \array_key_exists('from', $provider->getAttribute('options'))
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/sendgrid/:providerId')
+ ->desc('Update Sendgrid provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.update')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].update')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateSendgridProvider')
+ ->label('sdk.description', '/docs/references/messaging/update-sendgrid-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('name', '', new Text(128), 'Provider name.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true)
+ ->param('from', '', new Email(), 'Sender email address.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+ $providerAttr = $provider->getAttribute('provider');
+
+ if ($providerAttr !== 'sendgrid') {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ if (!empty($name)) {
+ $provider->setAttribute('name', $name);
+ }
+
+ if (!empty($from)) {
+ $provider->setAttribute('options', [
+ 'from' => $from,
+ ]);
+ }
+
+ if (!empty($apiKey)) {
+ $provider->setAttribute('credentials', [
+ 'apiKey' => $apiKey,
+ ]);
+ }
+
+ if ($enabled === true || $enabled === false) {
+ if (
+ $enabled === true
+ && \array_key_exists('apiKey', $provider->getAttribute('credentials'))
+ && \array_key_exists('from', $provider->getAttribute('options'))
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/msg91/:providerId')
+ ->desc('Update Msg91 provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.update')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].update')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateMsg91Provider')
+ ->label('sdk.description', '/docs/references/messaging/update-msg91-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('name', '', new Text(128), 'Provider name.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->param('senderId', '', new Text(0), 'Msg91 Sender ID.', true)
+ ->param('authKey', '', new Text(0), 'Msg91 Auth Key.', true)
+ ->param('from', '', new Text(256), 'Sender number.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $senderId, string $authKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+ $providerAttr = $provider->getAttribute('provider');
+
+ if ($providerAttr !== 'msg91') {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ if (!empty($name)) {
+ $provider->setAttribute('name', $name);
+ }
+
+ if (!empty($from)) {
+ $provider->setAttribute('options', [
+ 'from' => $from,
+ ]);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($senderId)) {
+ $credentials['senderId'] = $senderId;
+ }
+
+ if (!empty($authKey)) {
+ $credentials['authKey'] = $authKey;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ if ($enabled === true || $enabled === false) {
+ if (
+ $enabled === true
+ && \array_key_exists('senderId', $credentials)
+ && \array_key_exists('authKey', $credentials)
+ && \array_key_exists('from', $provider->getAttribute('options'))
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/telesign/:providerId')
+ ->desc('Update Telesign provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.update')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].update')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateTelesignProvider')
+ ->label('sdk.description', '/docs/references/messaging/update-telesign-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('name', '', new Text(128), 'Provider name.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->param('username', '', new Text(0), 'Telesign username.', true)
+ ->param('password', '', new Text(0), 'Telesign password.', true)
+ ->param('from', '', new Text(256), 'Sender number.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $password, string $from, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+ $providerAttr = $provider->getAttribute('provider');
+
+ if ($providerAttr !== 'telesign') {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ if (!empty($name)) {
+ $provider->setAttribute('name', $name);
+ }
+
+ if (!empty($from)) {
+ $provider->setAttribute('options', [
+ 'from' => $from,
+ ]);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($username)) {
+ $credentials['username'] = $username;
+ }
+
+ if (!empty($password)) {
+ $credentials['password'] = $password;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ if ($enabled === true || $enabled === false) {
+ if (
+ $enabled === true
+ && \array_key_exists('username', $credentials)
+ && \array_key_exists('password', $credentials)
+ && \array_key_exists('from', $provider->getAttribute('options'))
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/textmagic/:providerId')
+ ->desc('Update Textmagic provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.update')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].update')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateTextmagicProvider')
+ ->label('sdk.description', '/docs/references/messaging/update-textmagic-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('name', '', new Text(128), 'Provider name.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->param('username', '', new Text(0), 'Textmagic username.', true)
+ ->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true)
+ ->param('from', '', new Text(256), 'Sender number.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+ $providerAttr = $provider->getAttribute('provider');
+
+ if ($providerAttr !== 'textmagic') {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ if (!empty($name)) {
+ $provider->setAttribute('name', $name);
+ }
+
+ if (!empty($from)) {
+ $provider->setAttribute('options', [
+ 'from' => $from,
+ ]);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($username)) {
+ $credentials['username'] = $username;
+ }
+
+ if (!empty($apiKey)) {
+ $credentials['apiKey'] = $apiKey;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ if ($enabled === true || $enabled === false) {
+ if (
+ $enabled === true
+ && \array_key_exists('username', $credentials)
+ && \array_key_exists('apiKey', $credentials)
+ && \array_key_exists('from', $provider->getAttribute('options'))
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/twilio/:providerId')
+ ->desc('Update Twilio provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.update')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].update')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateTwilioProvider')
+ ->label('sdk.description', '/docs/references/messaging/update-twilio-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('name', '', new Text(128), 'Provider name.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true)
+ ->param('authToken', '', new Text(0), 'Twilio authentication token.', true)
+ ->param('from', '', new Text(256), 'Sender number.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $accountSid, string $authToken, string $from, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+ $providerAttr = $provider->getAttribute('provider');
+
+ if ($providerAttr !== 'twilio') {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ if (!empty($name)) {
+ $provider->setAttribute('name', $name);
+ }
+
+ if (!empty($from)) {
+ $provider->setAttribute('options', [
+ 'from' => $from,
+ ]);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($accountSid)) {
+ $credentials['accountSid'] = $accountSid;
+ }
+
+ if (!empty($authToken)) {
+ $credentials['authToken'] = $authToken;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ if ($enabled === true || $enabled === false) {
+ if (
+ $enabled === true
+ && \array_key_exists('accountSid', $credentials)
+ && \array_key_exists('authToken', $credentials)
+ && \array_key_exists('from', $provider->getAttribute('options'))
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/vonage/:providerId')
+ ->desc('Update Vonage provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.update')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].update')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateVonageProvider')
+ ->label('sdk.description', '/docs/references/messaging/update-vonage-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('name', '', new Text(128), 'Provider name.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->param('apiKey', '', new Text(0), 'Vonage API key.', true)
+ ->param('apiSecret', '', new Text(0), 'Vonage API secret.', true)
+ ->param('from', '', new Text(256), 'Sender number.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $apiSecret, string $from, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+ $providerAttr = $provider->getAttribute('provider');
+
+ if ($providerAttr !== 'vonage') {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ if (!empty($name)) {
+ $provider->setAttribute('name', $name);
+ }
+
+ if (!empty($from)) {
+ $provider->setAttribute('options', [
+ 'from' => $from,
+ ]);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($apiKey)) {
+ $credentials['apiKey'] = $apiKey;
+ }
+
+ if (!empty($apiSecret)) {
+ $credentials['apiSecret'] = $apiSecret;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ if ($enabled === true || $enabled === false) {
+ if (
+ $enabled === true
+ && \array_key_exists('apiKey', $credentials)
+ && \array_key_exists('apiSecret', $credentials)
+ && \array_key_exists('from', $provider->getAttribute('options'))
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/fcm/:providerId')
+ ->desc('Update FCM provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.update')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].update')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateFcmProvider')
+ ->label('sdk.description', '/docs/references/messaging/update-fcm-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('name', '', new Text(128), 'Provider name.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->param('serverKey', '', new Text(0), 'FCM Server Key.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $serverKey, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+ $providerAttr = $provider->getAttribute('provider');
+
+ if ($providerAttr !== 'fcm') {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ if (!empty($name)) {
+ $provider->setAttribute('name', $name);
+ }
+
+ if (!empty($serverKey)) {
+ $provider->setAttribute('credentials', ['serverKey' => $serverKey]);
+ }
+
+ if ($enabled === true || $enabled === false) {
+ if ($enabled === true && \array_key_exists('serverKey', $provider->getAttribute('credentials'))) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+
+App::patch('/v1/messaging/providers/apns/:providerId')
+ ->desc('Update APNS provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.update')
+ ->label('audits.resource', 'provider/{response.$id}')
+ ->label('event', 'providers.[providerId].update')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateApnsProvider')
+ ->label('sdk.description', '/docs/references/messaging/update-apns-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_PROVIDER)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('name', '', new Text(128), 'Provider name.', true)
+ ->param('enabled', null, new Boolean(), 'Set as enabled.', true)
+ ->param('authKey', '', new Text(0), 'APNS authentication key.', true)
+ ->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true)
+ ->param('teamId', '', new Text(0), 'APNS team ID.', true)
+ ->param('bundleId', '', new Text(0), 'APNS bundle ID.', true)
+ ->param('endpoint', '', new Text(0), 'APNS endpoint.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+ $providerAttr = $provider->getAttribute('provider');
+
+ if ($providerAttr !== 'apns') {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ if (!empty($name)) {
+ $provider->setAttribute('name', $name);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($authKey)) {
+ $credentials['authKey'] = $authKey;
+ }
+
+ if (!empty($authKeyId)) {
+ $credentials['authKeyId'] = $authKeyId;
+ }
+
+ if (!empty($teamId)) {
+ $credentials['teamId'] = $teamId;
+ }
+
+ if (!empty($bundleId)) {
+ $credentials['bundle'] = $bundleId;
+ }
+
+ if (!empty($endpoint)) {
+ $credentials['endpoint'] = $endpoint;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ if ($enabled === true || $enabled === false) {
+ if (
+ $enabled === true
+ && \array_key_exists('authKey', $credentials)
+ && \array_key_exists('authKeyId', $credentials)
+ && \array_key_exists('teamId', $credentials)
+ && \array_key_exists('bundleId', $credentials)
+ && \array_key_exists('endpoint', $credentials)
+ ) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::delete('/v1/messaging/providers/:providerId')
+ ->desc('Delete provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'provider.delete')
+ ->label('audits.resource', 'provider/{request.$providerId}')
+ ->label('event', 'providers.[providerId].delete')
+ ->label('scope', 'providers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'deleteProvider')
+ ->label('sdk.description', '/docs/references/messaging/delete-provider.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_NONE)
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+
+ $dbForProject->deleteDocument('providers', $provider->getId());
+
+ $queueForEvents
+ ->setParam('providerId', $provider->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_NOCONTENT)
+ ->noContent();
+ });
+
+App::post('/v1/messaging/topics')
+ ->desc('Create a topic.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'topic.create')
+ ->label('audits.resource', 'topic/{response.$id}')
+ ->label('event', 'topics.[topicId].create')
+ ->label('scope', 'topics.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createTopic')
+ ->label('sdk.description', '/docs/references/messaging/create-topic.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_TOPIC)
+ ->param('topicId', '', new CustomId(), 'Topic ID. Choose a custom Topic ID or a new Topic ID.')
+ ->param('name', '', new Text(128), 'Topic Name.')
+ ->param('description', '', new Text(2048), 'Topic Description.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $topicId, string $name, string $description, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $topicId = $topicId == 'unique()' ? ID::unique() : $topicId;
+
+ $topic = new Document([
+ '$id' => $topicId,
+ 'name' => $name,
+ ]);
+
+ if ($description) {
+ $topic->setAttribute('description', $description);
+ }
+
+ try {
+ $topic = $dbForProject->createDocument('topics', $topic);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::TOPIC_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('topicId', $topic->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($topic, Response::MODEL_TOPIC);
+ });
+
+App::get('/v1/messaging/topics')
+ ->desc('List topics.')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'topics.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'listTopics')
+ ->label('sdk.description', '/docs/references/messaging/list-topics.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_TOPIC_LIST)
+ ->param('queries', [], new Topics(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Topics::ALLOWED_ATTRIBUTES), true)
+ ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
+ $queries = Query::parseQueries($queries);
+
+ if (!empty($search)) {
+ $queries[] = Query::search('search', $search);
+ }
+
+ // Get cursor document if there was a cursor query
+ $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
+ $cursor = reset($cursor);
+
+ if ($cursor) {
+ $topicId = $cursor->getValue();
+ $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
+
+ if ($cursorDocument->isEmpty()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Topic '{$topicId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument[0]);
+ }
+
+ $response->dynamic(new Document([
+ 'topics' => $dbForProject->find('topics', $queries),
+ 'total' => $dbForProject->count('topics', $queries, APP_LIMIT_COUNT),
+ ]), Response::MODEL_TOPIC_LIST);
+ });
+
+App::get('/v1/messaging/topics/:topicId/logs')
+ ->desc('List topic logs')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'topics.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'listTopicLogs')
+ ->label('sdk.description', '/docs/references/messaging/topics/get-logs.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_LOG_LIST)
+ ->param('topicId', '', new UID(), 'Topic ID.')
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('locale')
+ ->inject('geodb')
+ ->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
+ $topic = $dbForProject->getDocument('topics', $topicId);
+
+ if ($topic->isEmpty()) {
+ throw new Exception(Exception::TOPIC_NOT_FOUND);
+ }
+
+ $queries = Query::parseQueries($queries);
+ $grouped = Query::groupByType($queries);
+ $limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
+ $offset = $grouped['offset'] ?? 0;
+
+ $audit = new Audit($dbForProject);
+ $resource = 'topic/' . $topicId;
+ $logs = $audit->getLogsByResource($resource, $limit, $offset);
+
+ $output = [];
+
+ foreach ($logs as $i => &$log) {
+ $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
+
+ $detector = new Detector($log['userAgent']);
+ $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
+
+ $os = $detector->getOS();
+ $client = $detector->getClient();
+ $device = $detector->getDevice();
+
+ $output[$i] = new Document([
+ 'event' => $log['event'],
+ 'userId' => ID::custom($log['data']['userId']),
+ 'userEmail' => $log['data']['userEmail'] ?? null,
+ 'userName' => $log['data']['userName'] ?? null,
+ 'mode' => $log['data']['mode'] ?? null,
+ 'ip' => $log['ip'],
+ 'time' => $log['time'],
+ 'osCode' => $os['osCode'],
+ 'osName' => $os['osName'],
+ 'osVersion' => $os['osVersion'],
+ 'clientType' => $client['clientType'],
+ 'clientCode' => $client['clientCode'],
+ 'clientName' => $client['clientName'],
+ 'clientVersion' => $client['clientVersion'],
+ 'clientEngine' => $client['clientEngine'],
+ 'clientEngineVersion' => $client['clientEngineVersion'],
+ 'deviceName' => $device['deviceName'],
+ 'deviceBrand' => $device['deviceBrand'],
+ 'deviceModel' => $device['deviceModel']
+ ]);
+
+ $record = $geodb->get($log['ip']);
+
+ if ($record) {
+ $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
+ $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
+ } else {
+ $output[$i]['countryCode'] = '--';
+ $output[$i]['countryName'] = $locale->getText('locale.country.unknown');
+ }
+ }
+
+ $response->dynamic(new Document([
+ 'total' => $audit->countLogsByResource($resource),
+ 'logs' => $output,
+ ]), Response::MODEL_LOG_LIST);
+ });
+
+App::get('/v1/messaging/topics/:topicId')
+ ->desc('Get a topic.')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'topics.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'getTopic')
+ ->label('sdk.description', '/docs/references/messaging/get-topic.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_TOPIC)
+ ->param('topicId', '', new UID(), 'Topic ID.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $topicId, Database $dbForProject, Response $response) {
+ $topic = $dbForProject->getDocument('topics', $topicId);
+
+ if ($topic->isEmpty()) {
+ throw new Exception(Exception::TOPIC_NOT_FOUND);
+ }
+
+ $topic = $dbForProject->getDocument('topics', $topicId);
+
+ $response
+ ->dynamic($topic, Response::MODEL_TOPIC);
+ });
+
+App::patch('/v1/messaging/topics/:topicId')
+ ->desc('Update a topic.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'topic.update')
+ ->label('audits.resource', 'topic/{response.$id}')
+ ->label('event', 'topics.[topicId].update')
+ ->label('scope', 'topics.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateTopic')
+ ->label('sdk.description', '/docs/references/messaging/update-topic.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_TOPIC)
+ ->param('topicId', '', new UID(), 'Topic ID.')
+ ->param('name', '', new Text(128), 'Topic Name.', true)
+ ->param('description', '', new Text(2048), 'Topic Description.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $topicId, string $name, string $description, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $topic = $dbForProject->getDocument('topics', $topicId);
+
+ if ($topic->isEmpty()) {
+ throw new Exception(Exception::TOPIC_NOT_FOUND);
+ }
+
+ if (!empty($name)) {
+ $topic->setAttribute('name', $name);
+ }
+
+ if (!empty($description)) {
+ $topic->setAttribute('description', $description);
+ }
+
+ $topic = $dbForProject->updateDocument('topics', $topicId, $topic);
+
+ $queueForEvents
+ ->setParam('topicId', $topic->getId());
+
+ $response
+ ->dynamic($topic, Response::MODEL_TOPIC);
+ });
+
+App::delete('/v1/messaging/topics/:topicId')
+ ->desc('Delete a topic.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'topic.delete')
+ ->label('audits.resource', 'topic/{request.$topicId}')
+ ->label('event', 'topics.[topicId].delete')
+ ->label('scope', 'topics.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'deleteTopic')
+ ->label('sdk.description', '/docs/references/messaging/delete-topic.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_NONE)
+ ->param('topicId', '', new UID(), 'Topic ID.')
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('queueForDeletes')
+ ->inject('response')
+ ->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, Delete $queueForDeletes, Response $response) {
+ $topic = $dbForProject->getDocument('topics', $topicId);
+
+ if ($topic->isEmpty()) {
+ throw new Exception(Exception::TOPIC_NOT_FOUND);
+ }
+
+ $dbForProject->deleteDocument('topics', $topicId);
+
+ $queueForDeletes
+ ->setType(DELETE_TYPE_TOPIC)
+ ->setDocument($topic);
+
+ $queueForEvents
+ ->setParam('topicId', $topic->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_NOCONTENT)
+ ->noContent();
+ });
+
+App::post('/v1/messaging/topics/:topicId/subscribers')
+ ->desc('Create a subscriber.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'subscriber.create')
+ ->label('audits.resource', 'subscriber/{response.$id}')
+ ->label('event', 'topics.[topicId].subscribers.[subscriberId].create')
+ ->label('scope', 'subscribers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_JWT, APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createSubscriber')
+ ->label('sdk.description', '/docs/references/messaging/create-subscriber.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_SUBSCRIBER)
+ ->param('subscriberId', '', new CustomId(), 'Subscriber ID. Choose a custom Subscriber ID or a new Subscriber ID.')
+ ->param('topicId', '', new UID(), 'Topic ID. The topic ID to subscribe to.')
+ ->param('targetId', '', new UID(), 'Target ID. The target ID to link to the specified Topic ID.')
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $subscriberId = $subscriberId == 'unique()' ? ID::unique() : $subscriberId;
+
+ $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
+
+ if ($topic->isEmpty()) {
+ throw new Exception(Exception::TOPIC_NOT_FOUND);
+ }
+
+ $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
+
+ if ($target->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
+
+ $subscriber = new Document([
+ '$id' => $subscriberId,
+ '$permissions' => [
+ Permission::read(Role::user($user->getId())),
+ Permission::delete(Role::user($user->getId())),
+ ],
+ 'topicId' => $topicId,
+ 'topicInternalId' => $topic->getInternalId(),
+ 'targetId' => $targetId,
+ 'targetInternalId' => $target->getInternalId(),
+ 'userId' => $user->getId(),
+ 'userInternalId' => $user->getInternalId(),
+ 'providerType' => $target->getAttribute('providerType'),
+ ]);
+
+ try {
+ $subscriber = $dbForProject->createDocument('subscribers', $subscriber);
+ Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('topics', $topicId, 'total', 1));
+ } catch (DuplicateException) {
+ throw new Exception(Exception::SUBSCRIBER_ALREADY_EXISTS);
+ }
+
+ $queueForEvents
+ ->setParam('topicId', $topic->getId())
+ ->setParam('subscriberId', $subscriber->getId());
+
+ $subscriber
+ ->setAttribute('target', $target)
+ ->setAttribute('userName', $user->getAttribute('name'));
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($subscriber, Response::MODEL_SUBSCRIBER);
+ });
+
+App::get('/v1/messaging/topics/:topicId/subscribers')
+ ->desc('List subscribers.')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'subscribers.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'listSubscribers')
+ ->label('sdk.description', '/docs/references/messaging/list-subscribers.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_SUBSCRIBER_LIST)
+ ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
+ ->param('queries', [], new Subscribers(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Providers::ALLOWED_ATTRIBUTES), true)
+ ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response) {
+ $queries = Query::parseQueries($queries);
+
+ if (!empty($search)) {
+ $queries[] = Query::search('search', $search);
+ }
+
+ $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
+
+ if ($topic->isEmpty()) {
+ throw new Exception(Exception::TOPIC_NOT_FOUND);
+ }
+
+ \array_push($queries, Query::equal('topicInternalId', [$topic->getInternalId()]));
+
+ // Get cursor document if there was a cursor query
+ $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
+ $cursor = reset($cursor);
+
+ if ($cursor) {
+ $subscriberId = $cursor->getValue();
+ $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId));
+
+ if ($cursorDocument->isEmpty()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscriber '{$subscriberId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $subscribers = $dbForProject->find('subscribers', $queries);
+
+ $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) {
+ return function () use ($subscriber, $dbForProject) {
+ $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
+ $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
+
+ return $subscriber
+ ->setAttribute('target', $target)
+ ->setAttribute('userName', $user->getAttribute('name'));
+ };
+ }, $subscribers));
+
+ $response
+ ->dynamic(new Document([
+ 'subscribers' => $subscribers,
+ 'total' => $dbForProject->count('subscribers', $queries, APP_LIMIT_COUNT),
+ ]), Response::MODEL_SUBSCRIBER_LIST);
+ });
+
+App::get('/v1/messaging/subscribers/:subscriberId/logs')
+ ->desc('List subscriber logs')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'subscribers.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'listSubscriberLogs')
+ ->label('sdk.description', '/docs/references/messaging/subscribers/get-logs.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_LOG_LIST)
+ ->param('subscriberId', '', new UID(), 'Subscriber ID.')
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('locale')
+ ->inject('geodb')
+ ->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
+ $subscriber = $dbForProject->getDocument('subscribers', $subscriberId);
+
+ if ($subscriber->isEmpty()) {
+ throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
+ }
+
+ $queries = Query::parseQueries($queries);
+ $grouped = Query::groupByType($queries);
+ $limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
+ $offset = $grouped['offset'] ?? 0;
+
+ $audit = new Audit($dbForProject);
+ $resource = 'subscriber/' . $subscriberId;
+ $logs = $audit->getLogsByResource($resource, $limit, $offset);
+
+ $output = [];
+
+ foreach ($logs as $i => &$log) {
+ $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
+
+ $detector = new Detector($log['userAgent']);
+ $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
+
+ $os = $detector->getOS();
+ $client = $detector->getClient();
+ $device = $detector->getDevice();
+
+ $output[$i] = new Document([
+ 'event' => $log['event'],
+ 'userId' => ID::custom($log['data']['userId']),
+ 'userEmail' => $log['data']['userEmail'] ?? null,
+ 'userName' => $log['data']['userName'] ?? null,
+ 'mode' => $log['data']['mode'] ?? null,
+ 'ip' => $log['ip'],
+ 'time' => $log['time'],
+ 'osCode' => $os['osCode'],
+ 'osName' => $os['osName'],
+ 'osVersion' => $os['osVersion'],
+ 'clientType' => $client['clientType'],
+ 'clientCode' => $client['clientCode'],
+ 'clientName' => $client['clientName'],
+ 'clientVersion' => $client['clientVersion'],
+ 'clientEngine' => $client['clientEngine'],
+ 'clientEngineVersion' => $client['clientEngineVersion'],
+ 'deviceName' => $device['deviceName'],
+ 'deviceBrand' => $device['deviceBrand'],
+ 'deviceModel' => $device['deviceModel']
+ ]);
+
+ $record = $geodb->get($log['ip']);
+
+ if ($record) {
+ $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
+ $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
+ } else {
+ $output[$i]['countryCode'] = '--';
+ $output[$i]['countryName'] = $locale->getText('locale.country.unknown');
+ }
+ }
+
+ $response->dynamic(new Document([
+ 'total' => $audit->countLogsByResource($resource),
+ 'logs' => $output,
+ ]), Response::MODEL_LOG_LIST);
+ });
+
+App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
+ ->desc('Get a subscriber.')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'subscribers.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'getSubscriber')
+ ->label('sdk.description', '/docs/references/messaging/get-subscriber.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_SUBSCRIBER)
+ ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
+ ->param('subscriberId', '', new UID(), 'Subscriber ID.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) {
+ $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
+
+ if ($topic->isEmpty()) {
+ throw new Exception(Exception::TOPIC_NOT_FOUND);
+ }
+
+ $subscriber = $dbForProject->getDocument('subscribers', $subscriberId);
+
+ if ($subscriber->isEmpty() || $subscriber->getAttribute('topicId') !== $topicId) {
+ throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
+ }
+
+ $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')));
+ $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId')));
+
+ $subscriber
+ ->setAttribute('target', $target)
+ ->setAttribute('userName', $user->getAttribute('name'));
+
+ $response
+ ->dynamic($subscriber, Response::MODEL_SUBSCRIBER);
+ });
+
+App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId')
+ ->desc('Delete a subscriber.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'subscriber.delete')
+ ->label('audits.resource', 'subscriber/{request.$subscriberId}')
+ ->label('event', 'topics.[topicId].subscribers.[subscriberId].delete')
+ ->label('scope', 'subscribers.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_JWT, APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'deleteSubscriber')
+ ->label('sdk.description', '/docs/references/messaging/delete-subscriber.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_NONE)
+ ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
+ ->param('subscriberId', '', new UID(), 'Subscriber ID.')
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) {
+ $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId));
+
+ if ($topic->isEmpty()) {
+ throw new Exception(Exception::TOPIC_NOT_FOUND);
+ }
+
+ $subscriber = $dbForProject->getDocument('subscribers', $subscriberId);
+
+ if ($subscriber->isEmpty() || $subscriber->getAttribute('topicId') !== $topicId) {
+ throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
+ }
+
+ $dbForProject->deleteDocument('subscribers', $subscriberId);
+ Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('topics', $topicId, 'total', 1));
+
+ $queueForEvents
+ ->setParam('topicId', $topic->getId())
+ ->setParam('subscriberId', $subscriber->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_NOCONTENT)
+ ->noContent();
+ });
+
+App::post('/v1/messaging/messages/email')
+ ->desc('Create an email.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'message.create')
+ ->label('audits.resource', 'message/{response.$id}')
+ ->label('event', 'messages.[messageId].create')
+ ->label('scope', 'messages.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createEmailMessage')
+ ->label('sdk.description', '/docs/references/messaging/create-email.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_MESSAGE)
+ ->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('subject', '', new Text(998), 'Email Subject.')
+ ->param('content', '', new Text(64230), 'Email Content.')
+ ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true)
+ ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true)
+ ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true)
+ ->param('description', '', new Text(256), 'Description for message.', true)
+ ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true)
+ ->param('html', false, new Boolean(), 'Is content of type HTML', true)
+ ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('project')
+ ->inject('queueForMessaging')
+ ->inject('response')
+ ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, string $description, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) {
+ $messageId = $messageId == 'unique()' ? ID::unique() : $messageId;
+
+ if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) {
+ throw new Exception(Exception::MESSAGE_MISSING_TARGET);
+ }
+
+ foreach ($targets as $target) {
+ $targetDocument = $dbForProject->getDocument('targets', $target);
+
+ if ($targetDocument->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_EMAIL) {
+ throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL . ' ' . $targetDocument->getId());
+ }
+ }
+
+ $message = $dbForProject->createDocument('messages', new Document([
+ '$id' => $messageId,
+ 'providerType' => MESSAGE_TYPE_EMAIL,
+ 'topics' => $topics,
+ 'users' => $users,
+ 'targets' => $targets,
+ 'description' => $description,
+ 'data' => [
+ 'subject' => $subject,
+ 'content' => $content,
+ 'html' => $html,
+ ],
+ 'status' => $status,
+ ]));
+
+ if ($status === 'processing') {
+ $queueForMessaging
+ ->setMessageId($message->getId())
+ ->setProject($project)
+ ->trigger();
+ }
+
+ $queueForEvents
+ ->setParam('messageId', $message->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($message, Response::MODEL_MESSAGE);
+ });
+
+App::post('/v1/messaging/messages/sms')
+ ->desc('Create an SMS.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'message.create')
+ ->label('audits.resource', 'message/{response.$id}')
+ ->label('event', 'messages.[messageId].create')
+ ->label('scope', 'messages.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createSMSMessage')
+ ->label('sdk.description', '/docs/references/messaging/create-sms.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_MESSAGE)
+ ->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('content', '', new Text(64230), 'SMS Content.')
+ ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true)
+ ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true)
+ ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true)
+ ->param('description', '', new Text(256), 'Description for Message.', true)
+ ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true)
+ ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('project')
+ ->inject('queueForMessaging')
+ ->inject('response')
+ ->action(function (string $messageId, string $content, array $topics, array $users, array $targets, string $description, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) {
+ $messageId = $messageId == 'unique()' ? ID::unique() : $messageId;
+
+ if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) {
+ throw new Exception(Exception::MESSAGE_MISSING_TARGET);
+ }
+
+ foreach ($targets as $target) {
+ $targetDocument = $dbForProject->getDocument('targets', $target);
+
+ if ($targetDocument->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_SMS) {
+ throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS . ' ' . $targetDocument->getId());
+ }
+ }
+
+ $message = $dbForProject->createDocument('messages', new Document([
+ '$id' => $messageId,
+ 'providerType' => MESSAGE_TYPE_SMS,
+ 'topics' => $topics,
+ 'users' => $users,
+ 'targets' => $targets,
+ 'description' => $description,
+ 'data' => [
+ 'content' => $content,
+ ],
+ 'status' => $status,
+ ]));
+
+ if ($status === 'processing') {
+ $queueForMessaging
+ ->setMessageId($message->getId())
+ ->setProject($project)
+ ->trigger();
+ }
+
+ $queueForEvents
+ ->setParam('messageId', $message->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($message, Response::MODEL_MESSAGE);
+ });
+
+App::post('/v1/messaging/messages/push')
+ ->desc('Create a push notification.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'message.create')
+ ->label('audits.resource', 'message/{response.$id}')
+ ->label('event', 'messages.[messageId].create')
+ ->label('scope', 'messages.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'createPushMessage')
+ ->label('sdk.description', '/docs/references/messaging/create-push-notification.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_MESSAGE)
+ ->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('title', '', new Text(256), 'Title for push notification.')
+ ->param('body', '', new Text(64230), 'Body for push notification.')
+ ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true)
+ ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true)
+ ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true)
+ ->param('description', '', new Text(256), 'Description for Message.', true)
+ ->param('data', null, new JSON(), 'Additional Data for push notification.', true)
+ ->param('action', '', new Text(256), 'Action for push notification.', true)
+ ->param('icon', '', new Text(256), 'Icon for push notification. Available only for Android and Web Platform.', true)
+ ->param('sound', '', new Text(256), 'Sound for push notification. Available only for Android and IOS Platform.', true)
+ ->param('color', '', new Text(256), 'Color for push notification. Available only for Android Platform.', true)
+ ->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true)
+ ->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true)
+ ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true)
+ ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('project')
+ ->inject('queueForMessaging')
+ ->inject('response')
+ ->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, string $description, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) {
+ $messageId = $messageId == 'unique()' ? ID::unique() : $messageId;
+
+ if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) {
+ throw new Exception(Exception::MESSAGE_MISSING_TARGET);
+ }
+
+ foreach ($targets as $target) {
+ $targetDocument = $dbForProject->getDocument('targets', $target);
+
+ if ($targetDocument->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
+ throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH . ' ' . $targetDocument->getId());
+ }
+ }
+
+ $pushData = [];
+
+ $keys = ['title', 'body', 'data', 'action', 'icon', 'sound', 'color', 'tag', 'badge'];
+
+ foreach ($keys as $key) {
+ if (!empty($$key)) {
+ $pushData[$key] = $$key;
+ }
+ }
+
+ $message = $dbForProject->createDocument('messages', new Document([
+ '$id' => $messageId,
+ 'providerType' => MESSAGE_TYPE_PUSH,
+ 'topics' => $topics,
+ 'users' => $users,
+ 'targets' => $targets,
+ 'description' => $description,
+ 'scheduledAt' => $scheduledAt,
+ 'data' => $pushData,
+ 'status' => $status,
+ ]));
+
+ if ($status === 'processing') {
+ $queueForMessaging
+ ->setMessageId($message->getId())
+ ->setProject($project)
+ ->trigger();
+ }
+
+ $queueForEvents
+ ->setParam('messageId', $message->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($message, Response::MODEL_MESSAGE);
+ });
+
+App::get('/v1/messaging/messages')
+ ->desc('List messages')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'messages.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'listMessages')
+ ->label('sdk.description', '/docs/references/messaging/list-messages.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_MESSAGE_LIST)
+ ->param('queries', [], new Messages(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Providers::ALLOWED_ATTRIBUTES), true)
+ ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (array $queries, string $search, Database $dbForProject, Response $response) {
+ $queries = Query::parseQueries($queries);
+
+ if (!empty($search)) {
+ $queries[] = Query::search('search', $search);
+ }
+
+ // Get cursor document if there was a cursor query
+ $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
+ $cursor = reset($cursor);
+
+ if ($cursor) {
+ $messageId = $cursor->getValue();
+ $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('messages', $messageId));
+
+ if ($cursorDocument->isEmpty()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $response->dynamic(new Document([
+ 'messages' => $dbForProject->find('messages', $queries),
+ 'total' => $dbForProject->count('messages', $queries, APP_LIMIT_COUNT),
+ ]), Response::MODEL_MESSAGE_LIST);
+ });
+
+App::get('/v1/messaging/messages/:messageId/logs')
+ ->desc('List message logs')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'messages.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'listMessageLogs')
+ ->label('sdk.description', '/docs/references/messaging/messages/get-logs.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_LOG_LIST)
+ ->param('messageId', '', new UID(), 'Message ID.')
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('locale')
+ ->inject('geodb')
+ ->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) {
+ $message = $dbForProject->getDocument('messages', $messageId);
+
+ if ($message->isEmpty()) {
+ throw new Exception(Exception::MESSAGE_NOT_FOUND);
+ }
+
+ $queries = Query::parseQueries($queries);
+ $grouped = Query::groupByType($queries);
+ $limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
+ $offset = $grouped['offset'] ?? 0;
+
+ $audit = new Audit($dbForProject);
+ $resource = 'message/' . $messageId;
+ $logs = $audit->getLogsByResource($resource, $limit, $offset);
+
+ $output = [];
+
+ foreach ($logs as $i => &$log) {
+ $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
+
+ $detector = new Detector($log['userAgent']);
+ $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
+
+ $os = $detector->getOS();
+ $client = $detector->getClient();
+ $device = $detector->getDevice();
+
+ $output[$i] = new Document([
+ 'event' => $log['event'],
+ 'userId' => ID::custom($log['data']['userId']),
+ 'userEmail' => $log['data']['userEmail'] ?? null,
+ 'userName' => $log['data']['userName'] ?? null,
+ 'mode' => $log['data']['mode'] ?? null,
+ 'ip' => $log['ip'],
+ 'time' => $log['time'],
+ 'osCode' => $os['osCode'],
+ 'osName' => $os['osName'],
+ 'osVersion' => $os['osVersion'],
+ 'clientType' => $client['clientType'],
+ 'clientCode' => $client['clientCode'],
+ 'clientName' => $client['clientName'],
+ 'clientVersion' => $client['clientVersion'],
+ 'clientEngine' => $client['clientEngine'],
+ 'clientEngineVersion' => $client['clientEngineVersion'],
+ 'deviceName' => $device['deviceName'],
+ 'deviceBrand' => $device['deviceBrand'],
+ 'deviceModel' => $device['deviceModel']
+ ]);
+
+ $record = $geodb->get($log['ip']);
+
+ if ($record) {
+ $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
+ $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
+ } else {
+ $output[$i]['countryCode'] = '--';
+ $output[$i]['countryName'] = $locale->getText('locale.country.unknown');
+ }
+ }
+
+ $response->dynamic(new Document([
+ 'total' => $audit->countLogsByResource($resource),
+ 'logs' => $output,
+ ]), Response::MODEL_LOG_LIST);
+ });
+
+App::get('/v1/messaging/messages/:messageId')
+ ->desc('Get a message')
+ ->groups(['api', 'messaging'])
+ ->label('scope', 'messages.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'getMessage')
+ ->label('sdk.description', '/docs/references/messaging/get-message.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_MESSAGE)
+ ->param('messageId', '', new UID(), 'Message ID.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $messageId, Database $dbForProject, Response $response) {
+ $message = $dbForProject->getDocument('messages', $messageId);
+
+ if ($message->isEmpty()) {
+ throw new Exception(Exception::MESSAGE_NOT_FOUND);
+ }
+
+ $response->dynamic($message, Response::MODEL_MESSAGE);
+ });
+
+App::patch('/v1/messaging/messages/email/:messageId')
+ ->desc('Update an email.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'message.update')
+ ->label('audits.resource', 'message/{response.$id}')
+ ->label('event', 'messages.[messageId].update')
+ ->label('scope', 'messages.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateEmail')
+ ->label('sdk.description', '/docs/references/messaging/update-email.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_MESSAGE)
+ ->param('messageId', '', new UID(), 'Message ID.')
+ ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true)
+ ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true)
+ ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true)
+ ->param('subject', '', new Text(998), 'Email Subject.', true)
+ ->param('description', '', new Text(256), 'Description for Message.', true)
+ ->param('content', '', new Text(64230), 'Email Content.', true)
+ ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true)
+ ->param('html', false, new Boolean(), 'Is content of type HTML', true)
+ ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('project')
+ ->inject('queueForMessaging')
+ ->inject('response')
+ ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $subject, string $description, string $content, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) {
+ $message = $dbForProject->getDocument('messages', $messageId);
+
+ if ($message->isEmpty()) {
+ throw new Exception(Exception::MESSAGE_NOT_FOUND);
+ }
+
+ if ($message->getAttribute('status') === 'sent') {
+ throw new Exception(Exception::MESSAGE_ALREADY_SENT);
+ }
+
+ if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) {
+ throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED);
+ }
+
+ if (!\is_null($topics)) {
+ $message->setAttribute('topics', $topics);
+ }
+
+ if (!\is_null($users)) {
+ $message->setAttribute('users', $users);
+ }
+
+ if (!\is_null($targets)) {
+ foreach ($targets as $target) {
+ $targetDocument = $dbForProject->getDocument('targets', $target);
+
+ if ($targetDocument->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_EMAIL) {
+ throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL . ' ' . $targetDocument->getId());
+ }
+ }
+
+ $message->setAttribute('targets', $targets);
+ }
+
+ $data = $message->getAttribute('data');
+
+ if (!empty($subject)) {
+ $data['subject'] = $subject;
+ }
+
+ if (!empty($content)) {
+ $data['content'] = $content;
+ }
+
+ if (!empty($html)) {
+ $data['html'] = $html;
+ }
+
+ $message->setAttribute('data', $data);
+
+ if (!empty($description)) {
+ $message->setAttribute('description', $description);
+ }
+
+ if (!empty($status)) {
+ $message->setAttribute('status', $status);
+ }
+
+ if (!is_null($scheduledAt)) {
+ $message->setAttribute('scheduledAt', $scheduledAt);
+ }
+
+ $message = $dbForProject->updateDocument('messages', $message->getId(), $message);
+
+ if ($status === 'processing') {
+ $queueForMessaging
+ ->setMessageId($message->getId())
+ ->setProject($project)
+ ->trigger();
+ }
+
+ $queueForEvents
+ ->setParam('messageId', $message->getId());
+
+ $response
+ ->dynamic($message, Response::MODEL_MESSAGE);
+ });
+
+App::patch('/v1/messaging/messages/sms/:messageId')
+ ->desc('Update an SMS.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'message.update')
+ ->label('audits.resource', 'message/{response.$id}')
+ ->label('event', 'messages.[messageId].update')
+ ->label('scope', 'messages.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updateSMS')
+ ->label('sdk.description', '/docs/references/messaging/update-email.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_MESSAGE)
+ ->param('messageId', '', new UID(), 'Message ID.')
+ ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Topic IDs.', true)
+ ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true)
+ ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true)
+ ->param('description', '', new Text(256), 'Description for Message.', true)
+ ->param('content', '', new Text(64230), 'Email Content.', true)
+ ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true)
+ ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('project')
+ ->inject('queueForMessaging')
+ ->inject('response')
+ ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $description, string $content, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) {
+ $message = $dbForProject->getDocument('messages', $messageId);
+
+ if ($message->isEmpty()) {
+ throw new Exception(Exception::MESSAGE_NOT_FOUND);
+ }
+
+ if ($message->getAttribute('status') === 'sent') {
+ throw new Exception(Exception::MESSAGE_ALREADY_SENT);
+ }
+
+ if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) {
+ throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED);
+ }
+
+ if (!\is_null($topics)) {
+ $message->setAttribute('topics', $topics);
+ }
+
+ if (!\is_null($users)) {
+ $message->setAttribute('users', $users);
+ }
+
+ if (!\is_null($targets)) {
+ foreach ($targets as $target) {
+ $targetDocument = $dbForProject->getDocument('targets', $target);
+
+ if ($targetDocument->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_SMS) {
+ throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS . ' ' . $targetDocument->getId());
+ }
+ }
+
+ $message->setAttribute('targets', $targets);
+ }
+
+ $data = $message->getAttribute('data');
+
+ if (!empty($content)) {
+ $data['content'] = $content;
+ }
+
+ $message->setAttribute('data', $data);
+
+ if (!empty($status)) {
+ $message->setAttribute('status', $status);
+ }
+
+ if (!empty($description)) {
+ $message->setAttribute('description', $description);
+ }
+
+ if (!is_null($scheduledAt)) {
+ $message->setAttribute('scheduledAt', $scheduledAt);
+ }
+
+ $message = $dbForProject->updateDocument('messages', $message->getId(), $message);
+
+ if ($status === 'processing') {
+ $queueForMessaging
+ ->setMessageId($message->getId())
+ ->setProject($project)
+ ->trigger();
+ }
+
+ $queueForEvents
+ ->setParam('messageId', $message->getId());
+
+ $response
+ ->dynamic($message, Response::MODEL_MESSAGE);
+ });
+
+App::patch('/v1/messaging/messages/push/:messageId')
+ ->desc('Update a push notification.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'message.update')
+ ->label('audits.resource', 'message/{response.$id}')
+ ->label('event', 'messages.[messageId].update')
+ ->label('scope', 'messages.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'updatePushNotification')
+ ->label('sdk.description', '/docs/references/messaging/update-push-notification.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_MESSAGE)
+ ->param('messageId', '', new UID(), 'Message ID.')
+ ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Topic IDs.', true)
+ ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true)
+ ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true)
+ ->param('description', '', new Text(256), 'Description for Message.', true)
+ ->param('title', '', new Text(256), 'Title for push notification.', true)
+ ->param('body', '', new Text(64230), 'Body for push notification.', true)
+ ->param('data', null, new JSON(), 'Additional Data for push notification.', true)
+ ->param('action', '', new Text(256), 'Action for push notification.', true)
+ ->param('icon', '', new Text(256), 'Icon for push notification. Available only for Android and Web Platform.', true)
+ ->param('sound', '', new Text(256), 'Sound for push notification. Available only for Android and IOS Platform.', true)
+ ->param('color', '', new Text(256), 'Color for push notification. Available only for Android Platform.', true)
+ ->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true)
+ ->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true)
+ ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true)
+ ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
+ ->inject('queueForEvents')
+ ->inject('dbForProject')
+ ->inject('project')
+ ->inject('queueForMessaging')
+ ->inject('response')
+ ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $description, string $title, string $body, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) {
+ $message = $dbForProject->getDocument('messages', $messageId);
+
+ if ($message->isEmpty()) {
+ throw new Exception(Exception::MESSAGE_NOT_FOUND);
+ }
+
+ if ($message->getAttribute('status') === 'sent') {
+ throw new Exception(Exception::MESSAGE_ALREADY_SENT);
+ }
+
+ if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) {
+ throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED);
+ }
+
+ if (!\is_null($topics)) {
+ $message->setAttribute('topics', $topics);
+ }
+
+ if (!\is_null($users)) {
+ $message->setAttribute('users', $users);
+ }
+
+ if (!\is_null($targets)) {
+ foreach ($targets as $target) {
+ $targetDocument = $dbForProject->getDocument('targets', $target);
+
+ if ($targetDocument->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
+ throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH . ' ' . $targetDocument->getId());
+ }
+ }
+
+ $message->setAttribute('targets', $targets);
+ }
+
+ $pushData = $message->getAttribute('data');
+
+ if ($title) {
+ $pushData['title'] = $title;
+ }
+
+ if ($body) {
+ $pushData['body'] = $body;
+ }
+
+ if (!is_null($data)) {
+ $pushData['data'] = $data;
+ }
+
+ if ($action) {
+ $pushData['action'] = $action;
+ }
+
+ if ($icon) {
+ $pushData['icon'] = $icon;
+ }
+
+ if ($sound) {
+ $pushData['sound'] = $sound;
+ }
+
+ if ($color) {
+ $pushData['color'] = $color;
+ }
+
+ if ($tag) {
+ $pushData['tag'] = $tag;
+ }
+
+ if ($badge) {
+ $pushData['badge'] = $badge;
+ }
+
+ $message->setAttribute('data', $pushData);
+
+ if (!empty($status)) {
+ $message->setAttribute('status', $status);
+ }
+
+ if (!empty($description)) {
+ $message->setAttribute('description', $description);
+ }
+
+ if (!is_null($scheduledAt)) {
+ $message->setAttribute('scheduledAt', $scheduledAt);
+ }
+
+ $message = $dbForProject->updateDocument('messages', $message->getId(), $message);
+
+ if ($status === 'processing') {
+ $queueForMessaging
+ ->setMessageId($message->getId())
+ ->setProject($project)
+ ->trigger();
+ }
+
+ $queueForEvents
+ ->setParam('messageId', $message->getId());
+
+ $response
+ ->dynamic($message, Response::MODEL_MESSAGE);
+ });
diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php
index b8f8ac4727..fe441e0e8c 100644
--- a/app/controllers/api/projects.php
+++ b/app/controllers/api/projects.php
@@ -159,7 +159,7 @@ App::post('/v1/projects')
'legalTaxId' => ID::custom($legalTaxId),
'services' => new stdClass(),
'platforms' => null,
- 'authProviders' => [],
+ 'oAuthProviders' => [],
'webhooks' => null,
'keys' => null,
'auths' => $auths,
@@ -598,7 +598,7 @@ App::patch('/v1/projects/:projectId/oauth2')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
- ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'Provider Name')
+ ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'Provider Name')
->param('appId', null, new Text(256), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new text(512), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Boolean(), 'Provider status. Set to \'false\' to disable new session creation.', true)
@@ -612,7 +612,7 @@ App::patch('/v1/projects/:projectId/oauth2')
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
- $providers = $project->getAttribute('authProviders', []);
+ $providers = $project->getAttribute('oAuthProviders', []);
if ($appId !== null) {
$providers[$provider . 'Appid'] = $appId;
@@ -626,7 +626,7 @@ App::patch('/v1/projects/:projectId/oauth2')
$providers[$provider . 'Enabled'] = $enabled;
}
- $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('authProviders', $providers));
+ $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('oAuthProviders', $providers));
$response->dynamic($project, Response::MODEL_PROJECT);
});
@@ -1151,7 +1151,7 @@ App::post('/v1/projects/:projectId/keys')
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
- ->param('expire', null, new DatetimeValidator(), 'Expiration time in ISO 8601 format. Use null for unlimited expiration.', true)
+ ->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) {
@@ -1268,7 +1268,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('keyId', '', new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
- ->param('expire', null, new DatetimeValidator(), 'Expiration time in ISO 8601 format. Use null for unlimited expiration.', true)
+ ->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) {
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index 2ee351f469..2ba27efcb3 100644
--- a/app/controllers/api/teams.php
+++ b/app/controllers/api/teams.php
@@ -6,7 +6,7 @@ use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
-use Appwrite\Event\Phone as EventPhone;
+use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
@@ -388,7 +388,7 @@ App::post('/v1/teams/:teamId/memberships')
->inject('queueForMails')
->inject('queueForMessaging')
->inject('queueForEvents')
- ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, EventPhone $queueForMessaging, Event $queueForEvents) {
+ ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents) {
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@@ -628,6 +628,10 @@ App::post('/v1/teams/:teamId/memberships')
->trigger()
;
} elseif (!empty($phone)) {
+ if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
+ throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
+ }
+
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? [];
@@ -638,9 +642,18 @@ App::post('/v1/teams/:teamId/memberships')
$message = $message->setParam('{{token}}', $url);
$message = $message->render();
+ $messageDoc = new Document([
+ '$id' => ID::unique(),
+ 'data' => [
+ 'content' => $message,
+ ],
+ ]);
+
$queueForMessaging
- ->setRecipient($phone)
- ->setMessage($message)
+ ->setMessage($messageDoc)
+ ->setRecipients([$phone])
+ ->setProviderType('SMS')
+ ->setProject($project)
->trigger();
}
}
diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php
index 0869453cc9..8a71f33d8b 100644
--- a/app/controllers/api/users.php
+++ b/app/controllers/api/users.php
@@ -9,6 +9,7 @@ use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Identities;
+use Appwrite\Utopia\Database\Validator\Queries\Targets;
use Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Users;
use Utopia\Database\Validator\Query\Limit;
@@ -98,6 +99,42 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name]),
]));
+
+ if ($email) {
+ try {
+ $target = $dbForProject->createDocument('targets', new Document([
+ 'userId' => $user->getId(),
+ 'userInternalId' => $user->getInternalId(),
+ 'providerType' => 'email',
+ 'identifier' => $email,
+ ]));
+ $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
+ } catch (Duplicate) {
+ $existingTarget = $dbForProject->findOne('targets', [
+ Query::equal('identifier', [$email]),
+ ]);
+ $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
+ }
+ }
+
+ if ($phone) {
+ try {
+ $target = $dbForProject->createDocument('targets', new Document([
+ 'userId' => $user->getId(),
+ 'userInternalId' => $user->getInternalId(),
+ 'providerType' => 'sms',
+ 'identifier' => $phone,
+ ]));
+ $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
+ } catch (Duplicate) {
+ $existingTarget = $dbForProject->findOne('targets', [
+ Query::equal('identifier', [$phone]),
+ ]);
+ $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
+ }
+ }
+
+ $dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@@ -379,6 +416,98 @@ App::post('/v1/users/scrypt-modified')
->dynamic($user, Response::MODEL_USER);
});
+App::post('/v1/users/:userId/targets')
+ ->desc('Create User Target')
+ ->groups(['api', 'users'])
+ ->label('audits.event', 'target.create')
+ ->label('audits.resource', 'target/response.$id')
+ ->label('event', 'users.[userId].targets.[targetId].create')
+ ->label('scope', 'targets.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
+ ->label('sdk.namespace', 'users')
+ ->label('sdk.method', 'createTarget')
+ ->label('sdk.description', '/docs/references/users/create-target.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_TARGET)
+ ->param('targetId', '', new CustomId(), 'Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
+ ->param('userId', '', new UID(), 'User ID.')
+ ->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.')
+ ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
+ ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true)
+ ->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true)
+ ->inject('queueForEvents')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->action(function (string $targetId, string $userId, string $providerType, string $identifier, string $providerId, string $name, Event $queueForEvents, Response $response, Database $dbForProject) {
+ $targetId = $targetId == 'unique()' ? ID::unique() : $targetId;
+
+ $provider = new Document();
+
+ if ($providerType === MESSAGE_TYPE_PUSH) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+ }
+
+ switch ($providerType) {
+ case 'email':
+ $validator = new Email();
+ if (!$validator->isValid($identifier)) {
+ throw new Exception(Exception::GENERAL_INVALID_EMAIL);
+ }
+ break;
+ case MESSAGE_TYPE_SMS:
+ $validator = new Phone();
+ if (!$validator->isValid($identifier)) {
+ throw new Exception(Exception::GENERAL_INVALID_PHONE);
+ }
+ break;
+ case MESSAGE_TYPE_PUSH:
+ break;
+ default:
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ $user = $dbForProject->getDocument('users', $userId);
+
+ if ($user->isEmpty()) {
+ throw new Exception(Exception::USER_NOT_FOUND);
+ }
+
+ $target = $dbForProject->getDocument('targets', $targetId);
+
+ if (!$target->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
+ }
+
+ try {
+ $target = $dbForProject->createDocument('targets', new Document([
+ '$id' => $targetId,
+ 'providerId' => $providerId ?? null,
+ 'providerInternalId' => $provider->getInternalId() ?? null,
+ 'providerType' => $providerType,
+ 'userId' => $userId,
+ 'userInternalId' => $user->getInternalId(),
+ 'identifier' => $identifier,
+ 'name' => ($name !== '') ? $name : null,
+ ]));
+ } catch (Duplicate) {
+ throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
+ }
+ $dbForProject->deleteCachedDocument('users', $user->getId());
+
+ $queueForEvents
+ ->setParam('userId', $user->getId())
+ ->setParam('targetId', $target->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($target, Response::MODEL_TARGET);
+ });
+
App::get('/v1/users')
->desc('List users')
->groups(['api', 'users'])
@@ -482,6 +611,38 @@ App::get('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
+App::get('/v1/users/:userId/targets/:targetId')
+ ->desc('Get User Target')
+ ->groups(['api', 'users'])
+ ->label('scope', 'targets.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
+ ->label('sdk.namespace', 'users')
+ ->label('sdk.method', 'getTarget')
+ ->label('sdk.description', '/docs/references/users/get-user-target.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_TARGET)
+ ->param('userId', '', new UID(), 'User ID.')
+ ->param('targetId', '', new UID(), 'Target ID.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->action(function (string $userId, string $targetId, Response $response, Database $dbForProject) {
+
+ $user = $dbForProject->getDocument('users', $userId);
+
+ if ($user->isEmpty()) {
+ throw new Exception(Exception::USER_NOT_FOUND);
+ }
+
+ $target = $user->find('$id', $targetId, 'targets');
+
+ if (empty($target)) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ $response->dynamic($target, Response::MODEL_TARGET);
+ });
+
App::get('/v1/users/:userId/sessions')
->desc('List user sessions')
->groups(['api', 'users'])
@@ -646,6 +807,53 @@ App::get('/v1/users/:userId/logs')
]), Response::MODEL_LOG_LIST);
});
+App::get('/v1/users/:userId/targets')
+ ->desc('List User Targets')
+ ->groups(['api', 'users'])
+ ->label('scope', 'targets.read')
+ ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
+ ->label('sdk.namespace', 'users')
+ ->label('sdk.method', 'listTargets')
+ ->label('sdk.description', '/docs/references/users/list-user-targets.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_TARGET_LIST)
+ ->param('userId', '', new UID(), 'User ID.')
+ ->param('queries', [], new Targets(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Users::ALLOWED_ATTRIBUTES), true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->action(function (string $userId, array $queries, Response $response, Database $dbForProject) {
+ $user = $dbForProject->getDocument('users', $userId);
+
+ if ($user->isEmpty()) {
+ throw new Exception(Exception::USER_NOT_FOUND);
+ }
+
+ $queries = Query::parseQueries($queries);
+
+ $queries[] = Query::equal('userId', [$userId]);
+
+ // Get cursor document if there was a cursor query
+ $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
+ $cursor = reset($cursor);
+
+ if ($cursor) {
+ $targetId = $cursor->getValue();
+ $cursorDocument = $dbForProject->getDocument('targets', $targetId);
+
+ if ($cursorDocument->isEmpty()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Target '{$targetId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $response->dynamic(new Document([
+ 'targets' => $dbForProject->find('targets', $queries),
+ 'total' => $dbForProject->count('targets', $queries, APP_LIMIT_COUNT),
+ ]), Response::MODEL_TARGET_LIST);
+ });
+
App::get('/v1/users/identities')
->desc('List Identities')
->groups(['api', 'users'])
@@ -949,6 +1157,16 @@ App::patch('/v1/users/:userId/email')
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
+ $target = $dbForProject->findOne('targets', [
+ Query::equal('identifier', [$email]),
+ ]);
+
+ if ($target instanceof Document && !$target->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
+ }
+
+ $oldEmail = $user->getAttribute('email');
+
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
@@ -957,6 +1175,15 @@ App::patch('/v1/users/:userId/email')
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
+ /**
+ * @var Document $oldTarget
+ */
+ $oldTarget = $user->find('identifier', $oldEmail, 'targets');
+
+ if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
+ $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email));
+ }
+ $dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@@ -994,13 +1221,32 @@ App::patch('/v1/users/:userId/phone')
throw new Exception(Exception::USER_NOT_FOUND);
}
+ $oldPhone = $user->getAttribute('phone');
+
$user
->setAttribute('phone', $number)
->setAttribute('phoneVerification', false)
;
+ $target = $dbForProject->findOne('targets', [
+ Query::equal('identifier', [$number]),
+ ]);
+
+ if ($target instanceof Document && !$target->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
+ }
+
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
+ /**
+ * @var Document $oldTarget
+ */
+ $oldTarget = $user->find('identifier', $oldPhone, 'targets');
+
+ if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
+ $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number));
+ }
+ $dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS);
}
@@ -1080,6 +1326,100 @@ App::patch('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
+App::patch('/v1/users/:userId/targets/:targetId')
+ ->desc('Update User target')
+ ->groups(['api', 'users'])
+ ->label('audits.event', 'target.update')
+ ->label('audits.resource', 'target/{response.$id}')
+ ->label('event', 'users.[userId].targets.[targetId].update')
+ ->label('scope', 'targets.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
+ ->label('sdk.namespace', 'users')
+ ->label('sdk.method', 'updateTarget')
+ ->label('sdk.description', '/docs/references/users/update-target.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_OK)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_TARGET)
+ ->param('userId', '', new UID(), 'User ID.')
+ ->param('targetId', '', new UID(), 'Target ID.')
+ ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)', true)
+ ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true)
+ ->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true)
+ ->inject('queueForEvents')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->action(function (string $userId, string $targetId, string $identifier, string $providerId, string $name, Event $queueForEvents, Response $response, Database $dbForProject) {
+ $user = $dbForProject->getDocument('users', $userId);
+
+ if ($user->isEmpty()) {
+ throw new Exception(Exception::USER_NOT_FOUND);
+ }
+
+ $target = $dbForProject->getDocument('targets', $targetId);
+
+ if ($target->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($user->getId() !== $target->getAttribute('userId')) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($identifier) {
+ $providerType = $target->getAttribute('providerType');
+
+ switch ($providerType) {
+ case 'email':
+ $validator = new Email();
+ if (!$validator->isValid($identifier)) {
+ throw new Exception(Exception::GENERAL_INVALID_EMAIL);
+ }
+ break;
+ case MESSAGE_TYPE_SMS:
+ $validator = new Phone();
+ if (!$validator->isValid($identifier)) {
+ throw new Exception(Exception::GENERAL_INVALID_PHONE);
+ }
+ break;
+ case MESSAGE_TYPE_PUSH:
+ break;
+ default:
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ $target->setAttribute('identifier', $identifier);
+ }
+
+ if ($providerId) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+
+ if ($provider->getAttribute('type') !== $target->getAttribute('providerType')) {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ $target->setAttribute('providerId', $provider->getId());
+ $target->setAttribute('providerInternalId', $provider->getInternalId());
+ }
+
+ if ($name) {
+ $target->setAttribute('name', $name);
+ }
+
+ $target = $dbForProject->updateDocument('targets', $target->getId(), $target);
+ $dbForProject->deleteCachedDocument('users', $user->getId());
+
+ $queueForEvents
+ ->setParam('userId', $user->getId())
+ ->setParam('targetId', $target->getId());
+
+ $response
+ ->dynamic($target, Response::MODEL_TARGET);
+ });
+
App::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete user session')
->groups(['api', 'users'])
@@ -1210,6 +1550,53 @@ App::delete('/v1/users/:userId')
$response->noContent();
});
+App::delete('/v1/users/:userId/targets/:targetId')
+ ->desc('Delete user target')
+ ->groups(['api', 'users'])
+ ->label('audits.event', 'target.delete')
+ ->label('audits.resource', 'target/{request.$targetId}')
+ ->label('event', 'users.[userId].targets.[targetId].delete')
+ ->label('scope', 'targets.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
+ ->label('sdk.namespace', 'users')
+ ->label('sdk.method', 'deleteTarget')
+ ->label('sdk.description', '/docs/references/users/delete-target.md')
+ ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_NONE)
+ ->param('userId', '', new UID(), 'User ID.')
+ ->param('targetId', '', new UID(), 'Target ID.')
+ ->inject('queueForEvents')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->action(function (string $userId, string $targetId, Event $queueForEvents, Response $response, Database $dbForProject) {
+
+ $user = $dbForProject->getDocument('users', $userId);
+
+ if ($user->isEmpty()) {
+ throw new Exception(Exception::USER_NOT_FOUND);
+ }
+
+ $target = $dbForProject->getDocument('targets', $targetId);
+
+ if ($target->isEmpty()) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ if ($user->getId() !== $target->getAttribute('userId')) {
+ throw new Exception(Exception::USER_TARGET_NOT_FOUND);
+ }
+
+ $dbForProject->deleteDocument('targets', $target->getId());
+ $dbForProject->deleteCachedDocument('users', $user->getId());
+
+ $queueForEvents
+ ->setParam('userId', $user->getId())
+ ->setParam('targetId', $target->getId());
+
+ $response->noContent();
+ });
+
App::delete('/v1/users/identities/:identityId')
->desc('Delete Identity')
->groups(['api', 'users'])
@@ -1251,7 +1638,7 @@ App::get('/v1/users/usage')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_USERS)
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
- ->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn ($value) => "oauth-" . $value, \array_keys(Config::getParam('providers', [])))), true), 'Provider Name.', true)
+ ->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn ($value) => "oauth-" . $value, \array_keys(Config::getParam('oAuthProviders', [])))), true), 'Provider Name.', true)
->inject('response')
->inject('dbForProject')
->inject('register')
diff --git a/app/init.php b/app/init.php
index 924122ac20..11cfef20ad 100644
--- a/app/init.php
+++ b/app/init.php
@@ -25,7 +25,7 @@ use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
-use Appwrite\Event\Phone;
+use Appwrite\Event\Messaging;
use Appwrite\Event\Delete;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Validator\Email;
@@ -47,13 +47,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Structure;
use Utopia\Locale\Locale;
use Utopia\DSN\DSN;
-use Utopia\Messaging\Adapters\SMS\Mock;
use Appwrite\GraphQL\Promises\Adapter\Swoole;
-use Utopia\Messaging\Adapters\SMS\Msg91;
-use Utopia\Messaging\Adapters\SMS\Telesign;
-use Utopia\Messaging\Adapters\SMS\TextMagic;
-use Utopia\Messaging\Adapters\SMS\Twilio;
-use Utopia\Messaging\Adapters\SMS\Vonage;
use Utopia\Registry\Registry;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Backblaze;
@@ -83,6 +77,7 @@ use Utopia\Validator\Range;
use Utopia\Validator\IP;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
+use Utopia\CLI\Console;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
@@ -103,6 +98,7 @@ const APP_LIMIT_COMPRESSION = 20000000; //20MB
const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements can there be in API parameter that expects array value
const APP_LIMIT_ARRAY_ELEMENT_SIZE = 4096; // Default maximum length of element in array parameter represented by maximum URL length.
const APP_LIMIT_SUBQUERY = 1000;
+const APP_LIMIT_SUBSCRIBERS_SUBQUERY = 1000000;
const APP_LIMIT_WRITE_RATE_DEFAULT = 60; // Default maximum write rate per rate period
const APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT = 60; // Default maximum write rate period in seconds
const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return in list API calls
@@ -173,6 +169,7 @@ const DELETE_TYPE_SESSIONS = 'sessions';
const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp';
const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
const DELETE_TYPE_SCHEDULES = 'schedules';
+const DELETE_TYPE_TOPIC = 'topic';
// Mail Types
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
@@ -189,6 +186,10 @@ const MAX_OUTPUT_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
// Function headers
const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host'];
const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length'];
+// Message types
+const MESSAGE_TYPE_EMAIL = 'email';
+const MESSAGE_TYPE_SMS = 'sms';
+const MESSAGE_TYPE_PUSH = 'push';
// Usage metrics
const METRIC_TEAMS = 'teams';
const METRIC_USERS = 'users';
@@ -233,7 +234,7 @@ App::setMode(App::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION));
Config::load('events', __DIR__ . '/config/events.php');
Config::load('auth', __DIR__ . '/config/auth.php');
Config::load('errors', __DIR__ . '/config/errors.php');
-Config::load('providers', __DIR__ . '/config/providers.php');
+Config::load('oAuthProviders', __DIR__ . '/config/oAuthProviders.php');
Config::load('platforms', __DIR__ . '/config/platforms.php');
Config::load('collections', __DIR__ . '/config/collections.php');
Config::load('runtimes', __DIR__ . '/config/runtimes.php');
@@ -522,6 +523,107 @@ Database::addFilter(
}
);
+Database::addFilter(
+ 'subQueryTargets',
+ function (mixed $value) {
+ return null;
+ },
+ function (mixed $value, Document $document, Database $database) {
+ return Authorization::skip(fn() => $database
+ ->find('targets', [
+ Query::equal('userInternalId', [$document->getInternalId()]),
+ Query::limit(APP_LIMIT_SUBQUERY)
+ ]));
+ }
+);
+
+Database::addFilter(
+ 'subQueryTopicTargets',
+ function (mixed $value) {
+ return null;
+ },
+ function (mixed $value, Document $document, Database $database) {
+ $targetIds = Authorization::skip(fn () => \array_map(
+ fn ($document) => $document->getAttribute('targetId'),
+ $database
+ ->find('subscribers', [
+ Query::equal('topicInternalId', [$document->getInternalId()]),
+ Query::limit(APP_LIMIT_SUBSCRIBERS_SUBQUERY)
+ ])
+ ));
+ if (\count($targetIds) > 0) {
+ return $database->find('targets', [Query::equal('$id', $targetIds)]);
+ }
+ return [];
+ }
+);
+
+Database::addFilter(
+ 'providerSearch',
+ function (mixed $value, Document $provider) {
+ $searchValues = [
+ $provider->getId(),
+ $provider->getAttribute('name', ''),
+ $provider->getAttribute('provider', ''),
+ $provider->getAttribute('type', '')
+ ];
+
+ $search = \implode(' ', \array_filter($searchValues));
+
+ return $search;
+ },
+ function (mixed $value) {
+ return $value;
+ }
+);
+
+Database::addFilter(
+ 'topicSearch',
+ function (mixed $value, Document $topic) {
+ $searchValues = [
+ $topic->getId(),
+ $topic->getAttribute('name', ''),
+ $topic->getAttribute('description', ''),
+ ];
+
+ $search = \implode(' ', \array_filter($searchValues));
+
+ return $search;
+ },
+ function (mixed $value) {
+ return $value;
+ }
+);
+
+Database::addFilter(
+ 'messageSearch',
+ function (mixed $value, Document $message) {
+ $searchValues = [
+ $message->getId(),
+ $message->getAttribute('description', ''),
+ $message->getAttribute('status', ''),
+ ];
+
+ $data = \json_decode($message->getAttribute('data', []), true);
+ $providerType = $message->getAttribute('providerType', '');
+
+ if ($providerType === MESSAGE_TYPE_EMAIL) {
+ $searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]);
+ } elseif ($providerType === MESSAGE_TYPE_SMS) {
+ $searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]);
+ } else {
+ $searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]);
+ }
+
+ $search = \implode(' ', \array_filter($searchValues));
+
+ return $search;
+ },
+ function (mixed $value) {
+ return $value;
+ }
+);
+
/**
* DB Formats
*/
@@ -902,7 +1004,7 @@ App::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
}, ['pools']);
App::setResource('queueForMessaging', function (Connection $queue) {
- return new Phone($queue);
+ return new Messaging($queue);
}, ['queue']);
App::setResource('queueForMails', function (Connection $queue) {
return new Mail($queue);
@@ -1118,7 +1220,7 @@ App::setResource('console', function () {
],
'authWhitelistEmails' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
'authWhitelistIPs' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [],
- 'authProviders' => [
+ 'oAuthProviders' => [
'githubEnabled' => true,
'githubSecret' => App::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''),
'githubAppid' => App::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '')
@@ -1342,21 +1444,6 @@ App::setResource('passwordsDictionary', function ($register) {
return $register->get('passwordsDictionary');
}, ['register']);
-App::setResource('sms', function () {
- $dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
- $user = $dsn->getUser();
- $secret = $dsn->getPassword();
-
- return match ($dsn->getHost()) {
- 'mock' => new Mock($user, $secret), // used for tests
- 'twilio' => new Twilio($user, $secret),
- 'text-magic' => new TextMagic($user, $secret),
- 'telesign' => new Telesign($user, $secret),
- 'msg91' => new Msg91($user, $secret),
- 'vonage' => new Vonage($user, $secret),
- default => null
- };
-});
App::setResource('servers', function () {
$platforms = Config::getParam('platforms');
diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml
index 898b46b3a5..252b5b6bd7 100644
--- a/app/views/install/compose.phtml
+++ b/app/views/install/compose.phtml
@@ -521,14 +521,20 @@ services:
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
+ - _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- - _APP_SMS_PROVIDER
- - _APP_SMS_FROM
+ - _APP_DB_HOST
+ - _APP_DB_PORT
+ - _APP_DB_SCHEMA
+ - _APP_DB_USER
+ - _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
+ - _APP_SMS_FROM
+ - _APP_SMS_PROVIDER
appwrite-worker-migrations:
image: /:
diff --git a/app/worker.php b/app/worker.php
index 4f7355311e..a8e5607965 100644
--- a/app/worker.php
+++ b/app/worker.php
@@ -13,7 +13,6 @@ use Appwrite\Event\Hamster;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
-use Appwrite\Event\Phone;
use Appwrite\Platform\Appwrite;
use Appwrite\Usage\Stats;
use Swoole\Runtime;
@@ -130,7 +129,7 @@ Server::setResource('queueForDatabase', function (Connection $queue) {
return new EventDatabase($queue);
}, ['queue']);
Server::setResource('queueForMessaging', function (Connection $queue) {
- return new Phone($queue);
+ return new Messaging($queue);
}, ['queue']);
Server::setResource('queueForMails', function (Connection $queue) {
return new Mail($queue);
diff --git a/docker-compose.yml b/docker-compose.yml
index 97cf7e5136..8fca5d67f0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -188,7 +188,9 @@ services:
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
- _APP_ASSISTANT_OPENAI_API_KEY
-
+ - _APP_MESSAGE_SMS_TEST_DSN
+ - _APP_MESSAGE_EMAIL_TEST_DSN
+ - _APP_MESSAGE_PUSH_TEST_DSN
appwrite-realtime:
entrypoint: realtime
<<: *x-logging
@@ -568,14 +570,20 @@ services:
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
+ - _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- - _APP_SMS_PROVIDER
- - _APP_SMS_FROM
+ - _APP_DB_HOST
+ - _APP_DB_PORT
+ - _APP_DB_SCHEMA
+ - _APP_DB_USER
+ - _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
+ - _APP_SMS_FROM
+ - _APP_SMS_PROVIDER
appwrite-worker-migrations:
entrypoint: worker-migrations
diff --git a/docs/tutorials/add-oauth2-provider.md b/docs/tutorials/add-oauth2-provider.md
index b3d81d1194..ab33f70cb2 100644
--- a/docs/tutorials/add-oauth2-provider.md
+++ b/docs/tutorials/add-oauth2-provider.md
@@ -37,7 +37,7 @@ Finally, you will need to create a `feat-XXX-YYY-oauth` branch based on the `mas
The first step in adding a new OAuth2 provider is to add it to the list of providers located at:
```
-app/config/providers.php
+app/config/oAuthProviders.php
```
Make sure to fill in all data needed and that your provider array key name:
@@ -45,7 +45,7 @@ Make sure to fill in all data needed and that your provider array key name:
- is in [`camelCase`](https://en.wikipedia.org/wiki/Camel_case) format for sentence, but lowercase for names. `github` must be all lowercased, but `paypalSandbox` should have uppercase S
- has no spaces or special characters
-> Please make sure to keep the list of providers in `providers.php` in the alphabetical order A-Z.
+> Please make sure to keep the list of providers in `oAuthProviders.php` in the alphabetical order A-Z.
### 2.2 Add Provider Logo
@@ -199,7 +199,7 @@ If you need any help with the contribution, feel free to head over to [our Disco
If your OAuth provider requires special configuration apart from `clientId` and `clientSecret` you can create a custom form. Currently this is being realized through putting all custom fields as JSON into the `clientSecret` field to keep the project API stable. You can implement your custom form following these steps:
-1. Add your custom form in `app/views/console/users/oauth/[PROVIDER].phtml`. Below is a template you can use. Add the filename to `app/config/providers.php`.
+1. Add your custom form in `app/views/console/users/oauth/[PROVIDER].phtml`. Below is a template you can use. Add the filename to `app/config/oAuthProviders.php`.
```php
./tests/e2e/Services/Projects
./tests/e2e/Services/Storage
./tests/e2e/Services/Webhooks
+ ./tests/e2e/Services/Messaging
./tests/e2e/Services/Functions/FunctionsBase.php
./tests/e2e/Services/Functions/FunctionsCustomServerTest.php
./tests/e2e/Services/Functions/FunctionsCustomClientTest.php
diff --git a/src/Appwrite/Auth/Validator/Password.php b/src/Appwrite/Auth/Validator/Password.php
index 93a9f74114..ffb72467e5 100644
--- a/src/Appwrite/Auth/Validator/Password.php
+++ b/src/Appwrite/Auth/Validator/Password.php
@@ -20,7 +20,7 @@ class Password extends Validator
*/
public function getDescription(): string
{
- return 'Password must be at least 8 characters';
+ return 'Password must be between 8 and 256 characters long.';
}
/**
@@ -40,6 +40,10 @@ class Password extends Validator
return false;
}
+ if (\strlen($value) > 256) {
+ return false;
+ }
+
return true;
}
diff --git a/src/Appwrite/Auth/Validator/PasswordDictionary.php b/src/Appwrite/Auth/Validator/PasswordDictionary.php
index 003d68bc73..e128f497f5 100644
--- a/src/Appwrite/Auth/Validator/PasswordDictionary.php
+++ b/src/Appwrite/Auth/Validator/PasswordDictionary.php
@@ -27,7 +27,7 @@ class PasswordDictionary extends Password
*/
public function getDescription(): string
{
- return 'Password must be at least 8 characters and should not be one of the commonly used password.';
+ return 'Password must be between 8 and 265 characters long, and should not be one of the commonly used password.';
}
/**
diff --git a/src/Appwrite/Event/Messaging.php b/src/Appwrite/Event/Messaging.php
new file mode 100644
index 0000000000..9201799355
--- /dev/null
+++ b/src/Appwrite/Event/Messaging.php
@@ -0,0 +1,173 @@
+setQueue(Event::MESSAGING_QUEUE_NAME)
+ ->setClass(Event::MESSAGING_CLASS_NAME);
+ }
+
+ /**
+ * Sets recipient for the messaging event.
+ *
+ * @param string[] $recipients
+ * @return self
+ */
+ public function setRecipients(array $recipients): self
+ {
+ $this->recipients = $recipients;
+
+ return $this;
+ }
+
+ /**
+ * Returns set recipient for messaging event.
+ *
+ * @return string[]
+ */
+ public function getRecipient(): array
+ {
+ return $this->recipients;
+ }
+
+ /**
+ * Sets message document for the messaging event.
+ *
+ * @param Document $message
+ * @return self
+ */
+ public function setMessage(Document $message): self
+ {
+ $this->message = $message;
+
+ return $this;
+ }
+
+ /**
+ * Returns message document for the messaging event.
+ *
+ * @return string
+ */
+ public function getMessage(): Document
+ {
+ return $this->message;
+ }
+
+ /**
+ * Sets message ID for the messaging event.
+ *
+ * @param string $message
+ * @return self
+ */
+ public function setMessageId(string $messageId): self
+ {
+ $this->messageId = $messageId;
+
+ return $this;
+ }
+
+ /**
+ * Returns set message ID for the messaging event.
+ *
+ * @return string
+ */
+ public function getMessageId(): string
+ {
+ return $this->messageId;
+ }
+
+ /**
+ * Sets provider type for the messaging event.
+ *
+ * @param string $providerType
+ * @return self
+ */
+ public function setProviderType(string $providerType): self
+ {
+ $this->providerType = $providerType;
+
+ return $this;
+ }
+
+ /**
+ * Returns set provider type for the messaging event.
+ *
+ * @return string
+ */
+ public function getProviderType(): string
+ {
+ return $this->providerType;
+ }
+
+ /**
+ * Sets Scheduled delivery time for the messaging event.
+ *
+ * @param string $scheduledAt
+ * @return self
+ */
+ public function setScheduledAt(string $scheduledAt): self
+ {
+ $this->scheduledAt = $scheduledAt;
+
+ return $this;
+ }
+
+ /**
+ * Returns set Delivery Time for the messaging event.
+ *
+ * @return string
+ */
+ public function getScheduledAt(): string
+ {
+ return $this->scheduledAt;
+ }
+
+ /**
+ * Set project for this event.
+ *
+ * @param Document $project
+ * @return self
+ */
+ public function setProject(Document $project): self
+ {
+ $this->project = $project;
+
+ return $this;
+ }
+
+ /**
+ * Executes the event and sends it to the messaging worker.
+ * @return string|bool
+ * @throws \InvalidArgumentException
+ */
+ public function trigger(): string | bool
+ {
+ $client = new Client($this->queue, $this->connection);
+
+ return $client->enqueue([
+ 'project' => $this->project,
+ 'user' => $this->user,
+ 'messageId' => $this->messageId,
+ 'message' => $this->message,
+ 'recipients' => $this->recipients,
+ 'providerType' => $this->providerType,
+ ]);
+ }
+}
diff --git a/src/Appwrite/Event/Phone.php b/src/Appwrite/Event/Phone.php
deleted file mode 100644
index 45f193a540..0000000000
--- a/src/Appwrite/Event/Phone.php
+++ /dev/null
@@ -1,87 +0,0 @@
-setQueue(Event::MESSAGING_QUEUE_NAME)
- ->setClass(Event::MESSAGING_CLASS_NAME);
- }
-
- /**
- * Sets recipient for the messaging event.
- *
- * @param string $recipient
- * @return self
- */
- public function setRecipient(string $recipient): self
- {
- $this->recipient = $recipient;
-
- return $this;
- }
-
- /**
- * Returns set recipient for this messaging event.
- *
- * @return string
- */
- public function getRecipient(): string
- {
- return $this->recipient;
- }
-
- /**
- * Sets url for the messaging event.
- *
- * @param string $message
- * @return self
- */
- public function setMessage(string $message): self
- {
- $this->message = $message;
-
- return $this;
- }
-
- /**
- * Returns set url for the messaging event.
- *
- * @return string
- */
- public function getMessage(): string
- {
- return $this->message;
- }
-
- /**
- * Executes the event and sends it to the messaging worker.
- *
- * @return string|bool
- * @throws \InvalidArgumentException
- */
- public function trigger(): string|bool
- {
- $client = new Client($this->queue, $this->connection);
-
- return $client->enqueue([
- 'project' => $this->project,
- 'user' => $this->user,
- 'payload' => $this->payload,
- 'recipient' => $this->recipient,
- 'message' => $this->message,
- 'events' => Event::generateEvents($this->getEvent(), $this->getParams())
- ]);
- }
-}
diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php
index 6449ffd93a..ea63423c05 100644
--- a/src/Appwrite/Extend/Exception.php
+++ b/src/Appwrite/Extend/Exception.php
@@ -55,6 +55,8 @@ class Exception extends \Exception
public const GENERAL_CODES_DISABLED = 'general_codes_disabled';
public const GENERAL_USAGE_DISABLED = 'general_usage_disabled';
public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented';
+ public const GENERAL_INVALID_EMAIL = 'general_invalid_email';
+ public const GENERAL_INVALID_PHONE = 'general_invalid_phone';
/** Users */
public const USER_COUNT_EXCEEDED = 'user_count_exceeded';
@@ -86,6 +88,8 @@ class Exception extends \Exception
public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error';
public const USER_EMAIL_ALREADY_VERIFIED = 'user_email_alread_verified';
public const USER_PHONE_ALREADY_VERIFIED = 'user_phone_already_verified';
+ public const USER_TARGET_NOT_FOUND = 'user_target_not_found';
+ public const USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists';
/** Teams */
public const TEAM_NOT_FOUND = 'team_not_found';
@@ -236,6 +240,30 @@ class Exception extends \Exception
/** Health */
public const QUEUE_SIZE_EXCEEDED = 'queue_size_exceeded';
+ /** Provider */
+ public const PROVIDER_NOT_FOUND = 'provider_not_found';
+ public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists';
+ public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type';
+ public const PROVIDER_INTERNAL_UPDATE_DISABLED = 'provider_internal_update_disabled';
+
+ /** Topic */
+ public const TOPIC_NOT_FOUND = 'topic_not_found';
+ public const TOPIC_ALREADY_EXISTS = 'topic_already_exists';
+
+ /** Subscriber */
+ public const SUBSCRIBER_NOT_FOUND = 'subscriber_not_found';
+ public const SUBSCRIBER_ALREADY_EXISTS = 'subscriber_already_exists';
+
+ /** Message */
+ public const MESSAGE_NOT_FOUND = 'message_not_found';
+ public const MESSAGE_MISSING_TARGET = 'message_missing_target';
+ public const MESSAGE_ALREADY_SENT = 'message_already_sent';
+ public const MESSAGE_ALREADY_SCHEDULED = 'message_already_scheduled';
+ public const MESSAGE_TARGET_NOT_EMAIL = 'message_target_not_email';
+ public const MESSAGE_TARGET_NOT_SMS = 'message_target_not_sms';
+ public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push';
+
+
protected string $type = '';
protected array $errors = [];
protected bool $publish = true;
diff --git a/src/Appwrite/Migration/Version/V15.php b/src/Appwrite/Migration/Version/V15.php
index 60f5fa20ab..1a8e8a4265 100644
--- a/src/Appwrite/Migration/Version/V15.php
+++ b/src/Appwrite/Migration/Version/V15.php
@@ -34,7 +34,7 @@ class V15 extends Migration
['email', 'anonymous'],
\array_map(
fn ($value) => "oauth-" . $value,
- \array_keys(Config::getParam('providers', []))
+ \array_keys(Config::getParam('oAuthProviders', []))
)
);
diff --git a/src/Appwrite/Migration/Version/V16.php b/src/Appwrite/Migration/Version/V16.php
index 1d56b246d6..49f244598e 100644
--- a/src/Appwrite/Migration/Version/V16.php
+++ b/src/Appwrite/Migration/Version/V16.php
@@ -124,23 +124,23 @@ class V16 extends Migration
/**
* Enable OAuth providers with data
*/
- $authProviders = $document->getAttribute('authProviders', []);
+ $oAuthProviders = $document->getAttribute('oAuthProviders', []);
- foreach (Config::getParam('providers') as $provider => $value) {
+ foreach (Config::getParam('oAuthProviders') as $provider => $value) {
if (!$value['enabled']) {
continue;
}
- if (($authProviders[$provider . 'Appid'] ?? false) && ($authProviders[$provider . 'Secret'] ?? false)) {
- if (array_key_exists($provider . 'Enabled', $authProviders)) {
+ if (($oAuthProviders[$provider . 'Appid'] ?? false) && ($oAuthProviders[$provider . 'Secret'] ?? false)) {
+ if (array_key_exists($provider . 'Enabled', $oAuthProviders)) {
continue;
}
- $authProviders[$provider . 'Enabled'] = true;
+ $oAuthProviders[$provider . 'Enabled'] = true;
}
}
- $document->setAttribute('authProviders', $authProviders);
+ $document->setAttribute('oAuthProviders', $oAuthProviders);
break;
}
diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php
index b95a13a12e..9a9e965f37 100644
--- a/src/Appwrite/Platform/Workers/Deletes.php
+++ b/src/Appwrite/Platform/Workers/Deletes.php
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Workers;
+use Appwrite\Auth\Auth;
use Executor\Executor;
use Throwable;
use Utopia\Abuse\Abuse;
@@ -149,6 +150,9 @@ class Deletes extends Action
case DELETE_TYPE_SCHEDULES:
$this->deleteSchedules($dbForConsole, $getProjectDB, $datetime);
break;
+ case DELETE_TYPE_TOPIC:
+ $this->deleteTopic($project, $getProjectDB, $document);
+ break;
default:
Console::error('No delete operation for type: ' . $type);
break;
@@ -193,6 +197,25 @@ class Deletes extends Action
);
}
+ /**
+ * @param Document $project
+ * @param callable $getProjectDB
+ * @param Document $topic
+ * @throws Exception
+ */
+ protected function deleteTopic(Document $project, callable $getProjectDB, Document $topic)
+ {
+ if ($topic->isEmpty()) {
+ Console::error('Failed to delete subscribers. Topic not found');
+ return;
+ }
+ $dbForProject = $getProjectDB($project);
+
+ $this->deleteByGroup('subscribers', [
+ Query::equal('topicInternalId', [$topic->getInternalId()])
+ ], $dbForProject);
+ }
+
/**
* @param Document $project
* @param callable $getProjectDB
@@ -533,6 +556,11 @@ class Deletes extends Action
$this->deleteByGroup('identities', [
Query::equal('userInternalId', [$userInternalId])
], $dbForProject);
+
+ // Delete targets
+ $this->deleteByGroup('targets', [
+ Query::equal('userInternalId', [$userInternalId])
+ ], $dbForProject);
}
/**
diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php
index 876ca50d0d..8f945e947f 100644
--- a/src/Appwrite/Platform/Workers/Messaging.php
+++ b/src/Appwrite/Platform/Workers/Messaging.php
@@ -2,30 +2,41 @@
namespace Appwrite\Platform\Workers;
-use Exception;
+use Appwrite\Extend\Exception;
use Utopia\App;
use Utopia\CLI\Console;
+use Utopia\Database\Helpers\ID;
use Utopia\DSN\DSN;
-use Utopia\Messaging\Messages\SMS;
+use Utopia\Platform\Action;
+use Utopia\Queue\Message;
+use Utopia\Database\Database;
+use Utopia\Database\DateTime;
+use Utopia\Database\Document;
+use Utopia\Database\Query;
+use Utopia\Messaging\Adapters\SMS as SMSAdapter;
use Utopia\Messaging\Adapters\SMS\Mock;
use Utopia\Messaging\Adapters\SMS\Msg91;
use Utopia\Messaging\Adapters\SMS\Telesign;
-use Utopia\Messaging\Adapters\SMS\TextMagic;
+use Utopia\Messaging\Adapters\SMS\Textmagic;
use Utopia\Messaging\Adapters\SMS\Twilio;
use Utopia\Messaging\Adapters\SMS\Vonage;
-use Utopia\Platform\Action;
-use Utopia\Queue\Message;
+use Utopia\Messaging\Adapters\Push as PushAdapter;
+use Utopia\Messaging\Adapters\Push\APNS;
+use Utopia\Messaging\Adapters\Push\FCM;
+use Utopia\Messaging\Adapters\Email as EmailAdapter;
+use Utopia\Messaging\Adapters\Email\Mailgun;
+use Utopia\Messaging\Adapters\Email\SendGrid;
+use Utopia\Messaging\Messages\Email;
+use Utopia\Messaging\Messages\Push;
+use Utopia\Messaging\Messages\SMS;
+
+use function Swoole\Coroutine\batch;
class Messaging extends Action
{
- private ?DSN $dsn = null;
- private string $user = '';
- private string $secret = '';
- private string $provider = '';
-
public static function getName(): string
{
- return 'messaging';
+ return "messaging";
}
/**
@@ -33,25 +44,20 @@ class Messaging extends Action
*/
public function __construct()
{
- $this->provider = App::getEnv('_APP_SMS_PROVIDER', '');
- if (!empty($this->provider)) {
- $this->dsn = new DSN($this->provider);
- $this->user = $this->dsn->getUser();
- $this->secret = $this->dsn->getPassword();
- }
-
$this
->desc('Messaging worker')
->inject('message')
- ->callback(fn($message) => $this->action($message));
+ ->inject('dbForProject')
+ ->callback(fn(Message $message, Database $dbForProject) => $this->action($message, $dbForProject));
}
/**
* @param Message $message
+ * @param Database $dbForProject
* @return void
* @throws Exception
*/
- public function action(Message $message): void
+ public function action(Message $message, Database $dbForProject): void
{
$payload = $message->getPayload() ?? [];
@@ -60,48 +66,318 @@ class Messaging extends Action
return;
}
- if (empty($payload['recipient'])) {
- Console::error('Recipient arg not found');
+ if (!\is_null($payload['message']) && !\is_null($payload['recipients'])) {
+ if ($payload['providerType'] === MESSAGE_TYPE_SMS) {
+ $this->processInternalSMSMessage(new Document($payload['message']), $payload['recipients']);
+ }
+ } else {
+ $message = $dbForProject->getDocument('messages', $payload['messageId']);
+
+ $this->processMessage($dbForProject, $message);
+ }
+ }
+
+ private function processMessage(Database $dbForProject, Document $message): void
+ {
+ $topicsId = $message->getAttribute('topics', []);
+ $targetsId = $message->getAttribute('targets', []);
+ $usersId = $message->getAttribute('users', []);
+
+ /**
+ * @var Document[] $recipients
+ */
+ $recipients = [];
+
+ if (\count($topicsId) > 0) {
+ $topics = $dbForProject->find('topics', [Query::equal('$id', $topicsId)]);
+ foreach ($topics as $topic) {
+ $targets = \array_filter($topic->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType'));
+ $recipients = \array_merge($recipients, $targets);
+ }
+ }
+
+ if (\count($usersId) > 0) {
+ $users = $dbForProject->find('users', [Query::equal('$id', $usersId)]);
+ foreach ($users as $user) {
+ $targets = \array_filter($user->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType'));
+ $recipients = \array_merge($recipients, $targets);
+ }
+ }
+
+ if (\count($targetsId) > 0) {
+ $targets = $dbForProject->find('targets', [Query::equal('$id', $targetsId)]);
+ $recipients = \array_merge($recipients, $targets);
+ }
+
+ $primaryProvider = $dbForProject->findOne('providers', [
+ Query::equal('enabled', [true]),
+ Query::equal('type', [$recipients[0]->getAttribute('providerType')]),
+ ]);
+
+ /**
+ * @var array> $identifiersByProviderId
+ */
+ $identifiersByProviderId = [];
+
+ /**
+ * @var Document[] $providers
+ */
+ $providers = [];
+ foreach ($recipients as $recipient) {
+ $providerId = $recipient->getAttribute('providerId');
+
+ if (!$providerId && $primaryProvider instanceof Document && !$primaryProvider->isEmpty()) {
+ $providerId = $primaryProvider->getId();
+ }
+
+ if ($providerId) {
+ if (!isset($identifiersByProviderId[$providerId])) {
+ $identifiersByProviderId[$providerId] = [];
+ }
+ $identifiersByProviderId[$providerId][] = $recipient->getAttribute('identifier');
+ }
+ }
+
+ /**
+ * @var array[] $results
+ */
+ $results = batch(\array_map(function ($providerId) use ($identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) {
+ return function () use ($providerId, $identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) {
+ $provider = new Document();
+
+ if ($primaryProvider->getId() === $providerId) {
+ $provider = $primaryProvider;
+ } else {
+ $provider = $dbForProject->getDocument('providers', $providerId, [Query::equal('enabled', [true])]);
+
+ if ($provider->isEmpty()) {
+ $provider = $primaryProvider;
+ }
+ }
+
+ $providers[] = $provider;
+ $identifiers = $identifiersByProviderId[$providerId];
+
+ $adapter = match ($provider->getAttribute('type')) {
+ MESSAGE_TYPE_SMS => $this->sms($provider),
+ MESSAGE_TYPE_PUSH => $this->push($provider),
+ MESSAGE_TYPE_EMAIL => $this->email($provider),
+ default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
+ };
+
+ $maxBatchSize = $adapter->getMaxMessagesPerRequest();
+ $batches = \array_chunk($identifiers, $maxBatchSize);
+ $batchIndex = 0;
+
+ $results = batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) {
+ return function () use ($batch, $message, $provider, $adapter, $batchIndex) {
+ $deliveredTotal = 0;
+ $deliveryErrors = [];
+ $messageData = clone $message;
+ $messageData->setAttribute('to', $batch);
+
+ $data = match ($provider->getAttribute('type')) {
+ MESSAGE_TYPE_SMS => $this->buildSMSMessage($messageData, $provider),
+ MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData),
+ MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($messageData, $provider),
+ default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
+ };
+
+ try {
+ $adapter->send($data);
+ $deliveredTotal += \count($batch);
+ } catch (\Exception $e) {
+ $deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage();
+ } finally {
+ $batchIndex++;
+ return [
+ 'deliveredTotal' => $deliveredTotal,
+ 'deliveryErrors' => $deliveryErrors,
+ ];
+ }
+ };
+ }, $batches));
+
+ return $results;
+ };
+ }, \array_keys($identifiersByProviderId)));
+
+ $results = array_merge(...$results);
+
+ $deliveredTotal = 0;
+ $deliveryErrors = [];
+
+ foreach ($results as $result) {
+ $deliveredTotal += $result['deliveredTotal'];
+ $deliveryErrors = \array_merge($deliveryErrors, $result['deliveryErrors']);
+ }
+
+ $message->setAttribute('deliveryErrors', $deliveryErrors);
+
+ if (\count($message->getAttribute('deliveryErrors')) > 0) {
+ $message->setAttribute('status', 'failed');
+ } else {
+ $message->setAttribute('status', 'sent');
+ }
+
+ $message->removeAttribute('to');
+
+ foreach ($providers as $provider) {
+ $message->setAttribute('search', "{$message->getAttribute('search')} {$provider->getAttribute('name')} {$provider->getAttribute('provider')} {$provider->getAttribute('type')}");
+ }
+
+ $message->setAttribute('deliveredTotal', $deliveredTotal);
+ $message->setAttribute('deliveredAt', DateTime::now());
+
+ $dbForProject->updateDocument('messages', $message->getId(), $message);
+ }
+
+ private function processInternalSMSMessage(Document $message, array $recipients): void
+ {
+ if (empty(App::getEnv('_APP_SMS_PROVIDER')) || empty(App::getEnv('_APP_SMS_FROM'))) {
+ Console::info('Skipped SMS processing. No Phone configuration has been set.');
return;
}
- if (empty($payload['message'])) {
- Console::error('Message arg not found');
- return;
- }
-
- $sms = match ($this->dsn->getHost()) {
- 'mock' => new Mock($this->user, $this->secret), // used for tests
- 'twilio' => new Twilio($this->user, $this->secret),
- 'text-magic' => new TextMagic($this->user, $this->secret),
- 'telesign' => new Telesign($this->user, $this->secret),
- 'msg91' => new Msg91($this->user, $this->secret),
- 'vonage' => new Vonage($this->user, $this->secret),
- default => null
- };
-
- if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
- Console::error('Skipped sms processing. No Phone provider has been set.');
- return;
- }
+ $smsDSN = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
+ $host = $smsDSN->getHost();
+ $password = $smsDSN->getPassword();
+ $user = $smsDSN->getUser();
$from = App::getEnv('_APP_SMS_FROM');
- if (empty($from)) {
- Console::error('Skipped sms processing. No phone number has been set.');
- return;
- }
+ $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
+ ],
+ 'textmagic' => [
+ 'username' => $user,
+ 'apiKey' => $password
+ ],
+ 'telesign' => [
+ 'username' => $user,
+ 'password' => $password
+ ],
+ 'msg91' => [
+ 'senderId' => $user,
+ 'authKey' => $password
+ ],
+ 'vonage' => [
+ 'apiKey' => $user,
+ 'apiSecret' => $password
+ ],
+ default => null
+ },
+ 'options' => [
+ 'from' => $from
+ ]
+ ]);
- $message = new SMS(
- to: [$payload['recipient']],
- content: $payload['message'],
- from: $from,
- );
+ $adapter = $this->sms($provider);
- try {
- $sms->send($message);
- } catch (\Exception $error) {
- throw new Exception('Error sending message: ' . $error->getMessage(), 500);
- }
+ $maxBatchSize = $adapter->getMaxMessagesPerRequest();
+ $batches = \array_chunk($recipients, $maxBatchSize);
+ $batchIndex = 0;
+
+ batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) {
+ return function () use ($batch, $message, $provider, $adapter, $batchIndex) {
+ $message->setAttribute('to', $batch);
+
+ $data = $this->buildSMSMessage($message, $provider);
+
+ try {
+ $adapter->send($data);
+ } catch (\Exception $e) {
+ Console::error('Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage());
+ }
+ };
+ }, $batches));
+ }
+
+ public function shutdown(): void
+ {
+ }
+
+ private function sms(Document $provider): ?SMSAdapter
+ {
+ $credentials = $provider->getAttribute('credentials');
+ return match ($provider->getAttribute('provider')) {
+ 'mock' => new Mock('username', 'password'),
+ 'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']),
+ 'textmagic' => new Textmagic($credentials['username'], $credentials['apiKey']),
+ 'telesign' => new Telesign($credentials['username'], $credentials['password']),
+ 'msg91' => new Msg91($credentials['senderId'], $credentials['authKey']),
+ 'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']),
+ default => null
+ };
+ }
+
+ private function push(Document $provider): ?PushAdapter
+ {
+ $credentials = $provider->getAttribute('credentials');
+ return match ($provider->getAttribute('provider')) {
+ 'apns' => new APNS(
+ $credentials['authKey'],
+ $credentials['authKeyId'],
+ $credentials['teamId'],
+ $credentials['bundleId'],
+ $credentials['endpoint']
+ ),
+ 'fcm' => new FCM($credentials['serverKey']),
+ default => null
+ };
+ }
+
+ private function email(Document $provider): ?EmailAdapter
+ {
+ $credentials = $provider->getAttribute('credentials');
+ return match ($provider->getAttribute('provider')) {
+ 'mailgun' => new Mailgun($credentials['apiKey'], $credentials['domain'], $credentials['isEuRegion']),
+ 'sendgrid' => new SendGrid($credentials['apiKey']),
+ default => null
+ };
+ }
+
+ private function buildEmailMessage(Document $message, Document $provider): Email
+ {
+ $from = $provider['options']['from'];
+ $to = $message['to'];
+ $subject = $message['data']['subject'];
+ $content = $message['data']['content'];
+ $html = $message['data']['html'];
+
+ return new Email($to, $subject, $content, $from, null, $html);
+ }
+
+ private function buildSMSMessage(Document $message, Document $provider): SMS
+ {
+ $to = $message['to'];
+ $content = $message['data']['content'];
+ $from = $provider['options']['from'];
+
+ return new SMS($to, $content, $from);
+ }
+
+ private function buildPushMessage(Document $message): Push
+ {
+ $to = $message['to'];
+ $title = $message['data']['title'];
+ $body = $message['data']['body'];
+ $data = $message['data']['data'];
+ $action = $message['data']['action'];
+ $sound = $message['data']['sound'];
+ $icon = $message['data']['icon'];
+ $color = $message['data']['color'];
+ $tag = $message['data']['tag'];
+ $badge = $message['data']['badge'];
+
+ return new Push($to, $title, $body, $data, $action, $sound, $icon, $color, $tag, $badge);
}
}
diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Messages.php b/src/Appwrite/Utopia/Database/Validator/Queries/Messages.php
new file mode 100644
index 0000000000..dd043474a8
--- /dev/null
+++ b/src/Appwrite/Utopia/Database/Validator/Queries/Messages.php
@@ -0,0 +1,28 @@
+setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false))
->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false))
->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false))
- ->setModel(new BaseList('Providers List', self::MODEL_PROVIDER_LIST, 'platforms', self::MODEL_PROVIDER, true, false))
+ ->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false))
->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false))
->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY))
->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT))
@@ -328,6 +345,11 @@ class Response extends SwooleResponse
->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS))
->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE))
->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE))
+ ->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER))
+ ->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE))
+ ->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC))
+ ->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER))
+ ->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET))
->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION))
->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT))
// Entities
@@ -380,7 +402,7 @@ class Response extends SwooleResponse
->setModel(new Project())
->setModel(new Webhook())
->setModel(new Key())
- ->setModel(new Provider())
+ ->setModel(new AuthProvider())
->setModel(new Platform())
->setModel(new Variable())
->setModel(new Country())
@@ -408,6 +430,11 @@ class Response extends SwooleResponse
->setModel(new TemplateSMS())
->setModel(new TemplateEmail())
->setModel(new ConsoleVariables())
+ ->setModel(new Provider())
+ ->setModel(new Message())
+ ->setModel(new Topic())
+ ->setModel(new Subscriber())
+ ->setModel(new Target())
->setModel(new Migration())
->setModel(new MigrationReport())
->setModel(new MigrationFirebaseProject())
diff --git a/src/Appwrite/Utopia/Response/Filters/V13.php b/src/Appwrite/Utopia/Response/Filters/V13.php
index d48473593e..04f9782915 100644
--- a/src/Appwrite/Utopia/Response/Filters/V13.php
+++ b/src/Appwrite/Utopia/Response/Filters/V13.php
@@ -60,8 +60,8 @@ class V13 extends Filter
protected function parseProject($content)
{
- $content['providers'] = $content['authProviders'];
- unset($content['authProviders']);
+ $content['providers'] = $content['oAuthProviders'];
+ unset($content['oAuthProviders']);
return $content;
}
diff --git a/src/Appwrite/Utopia/Response/Filters/V16.php b/src/Appwrite/Utopia/Response/Filters/V16.php
index 6943bd8f4d..609f118f6e 100644
--- a/src/Appwrite/Utopia/Response/Filters/V16.php
+++ b/src/Appwrite/Utopia/Response/Filters/V16.php
@@ -88,9 +88,9 @@ class V16 extends Filter
protected function parseProject(array $content)
{
- foreach ($content['providers'] ?? [] as $i => $provider) {
- $content['providers'][$i]['name'] = \ucfirst($provider['key']);
- unset($content['providers'][$i]['key']);
+ foreach ($content['oAuthProviders'] ?? [] as $i => $provider) {
+ $content['oAuthProviders'][$i]['name'] = \ucfirst($provider['key']);
+ unset($content['oAuthProviders'][$i]['key']);
}
$content['domains'] = [];
diff --git a/src/Appwrite/Utopia/Response/Model/AuthProvider.php b/src/Appwrite/Utopia/Response/Model/AuthProvider.php
new file mode 100644
index 0000000000..0171a3c152
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/AuthProvider.php
@@ -0,0 +1,69 @@
+addRule('key', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Auth Provider.',
+ 'default' => '',
+ 'example' => 'github',
+ ])
+ ->addRule('name', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Auth Provider name.',
+ 'default' => '',
+ 'example' => 'GitHub',
+ ])
+ ->addRule('appId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'OAuth 2.0 application ID.',
+ 'default' => '',
+ 'example' => '259125845563242502',
+ ])
+ ->addRule('secret', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration.',
+ 'default' => '',
+ 'example' => 'Bpw_g9c2TGXxfgLshDbSaL8tsCcqgczQ',
+ ])
+ ->addRule('enabled', [
+ 'type' => self::TYPE_BOOLEAN,
+ 'description' => 'Auth Provider is active and can be used to create session.',
+ 'example' => '',
+ ])
+ ;
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'AuthProvider';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_AUTH_PROVIDER;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Message.php b/src/Appwrite/Utopia/Response/Model/Message.php
new file mode 100644
index 0000000000..791c87933f
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/Message.php
@@ -0,0 +1,131 @@
+addRule('$id', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Message ID.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('$createdAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Message creation time in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('$updatedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Message update date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('providerType', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Message provider type.',
+ 'default' => '',
+ 'example' => MESSAGE_TYPE_EMAIL,
+ ])
+ ->addRule('topics', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Topic IDs set as recipients.',
+ 'default' => '',
+ 'array' => true,
+ 'example' => ['5e5ea5c16897e'],
+ ])
+ ->addRule('users', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'User IDs set as recipients.',
+ 'default' => '',
+ 'array' => true,
+ 'example' => ['5e5ea5c16897e'],
+ ])
+ ->addRule('targets', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Target IDs set as recipients.',
+ 'default' => '',
+ 'array' => true,
+ 'example' => ['5e5ea5c16897e'],
+ ])
+ ->addRule('scheduledAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'The scheduled time for message.',
+ 'required' => false,
+ 'default' => DateTime::now(),
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('deliveredAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'The time when the message was delivered.',
+ 'required' => false,
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('deliveryErrors', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Delivery errors if any.',
+ 'required' => false,
+ 'default' => '',
+ 'array' => true,
+ 'example' => ['Failed to send message to target 5e5ea5c16897e: Credentials not valid.'],
+ ])
+ ->addRule('deliveredTotal', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Number of recipients the message was delivered to.',
+ 'default' => 0,
+ 'example' => 1,
+ ])
+ ->addRule('data', [
+ 'type' => self::TYPE_JSON,
+ 'description' => 'Data of the message.',
+ 'default' => [],
+ 'example' => [
+ 'subject' => 'Welcome to Appwrite',
+ 'content' => 'Hi there, welcome to Appwrite family.',
+ ],
+ ])
+ ->addRule('status', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Status of delivery.',
+ 'default' => 'processing',
+ 'example' => 'Message status can be one of the following: processing, sent, cancelled, failed.',
+ ])
+ ->addRule('description', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Message description.',
+ 'required' => false,
+ 'default' => '',
+ 'example' => 'Welcome Email.',
+ ]);
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'Message';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_MESSAGE;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php
index 20703ffbeb..6bab4401d7 100644
--- a/src/Appwrite/Utopia/Response/Model/Project.php
+++ b/src/Appwrite/Utopia/Response/Model/Project.php
@@ -138,9 +138,9 @@ class Project extends Model
'default' => false,
'example' => true,
])
- ->addRule('providers', [
- 'type' => Response::MODEL_PROVIDER,
- 'description' => 'List of Providers.',
+ ->addRule('oAuthProviders', [
+ 'type' => Response::MODEL_AUTH_PROVIDER,
+ 'description' => 'List of Auth Providers.',
'default' => [],
'example' => [new \stdClass()],
'array' => true,
@@ -328,9 +328,9 @@ class Project extends Model
$document->setAttribute('auth' . ucfirst($key), $value);
}
- // Providers
- $providers = Config::getParam('providers', []);
- $providerValues = $document->getAttribute('authProviders', []);
+ // OAuth Providers
+ $providers = Config::getParam('oAuthProviders', []);
+ $providerValues = $document->getAttribute('oAuthProviders', []);
$projectProviders = [];
foreach ($providers as $key => $provider) {
@@ -348,7 +348,7 @@ class Project extends Model
]);
}
- $document->setAttribute("providers", $projectProviders);
+ $document->setAttribute('oAuthProviders', $projectProviders);
return $document;
}
diff --git a/src/Appwrite/Utopia/Response/Model/Provider.php b/src/Appwrite/Utopia/Response/Model/Provider.php
index c589011a46..d3de061aab 100644
--- a/src/Appwrite/Utopia/Response/Model/Provider.php
+++ b/src/Appwrite/Utopia/Response/Model/Provider.php
@@ -7,44 +7,68 @@ use Appwrite\Utopia\Response\Model;
class Provider extends Model
{
- /**
- * @var bool
- */
- protected bool $public = false;
-
public function __construct()
{
$this
- ->addRule('key', [
+ ->addRule('$id', [
'type' => self::TYPE_STRING,
- 'description' => 'Provider.',
+ 'description' => 'Provider ID.',
'default' => '',
- 'example' => 'github',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('$createdAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Provider creation time in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('$updatedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Provider update date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
- 'description' => 'Provider name.',
+ 'description' => 'The name for the provider instance.',
'default' => '',
- 'example' => 'GitHub',
+ 'example' => 'Mailgun',
])
- ->addRule('appId', [
+ ->addRule('provider', [
'type' => self::TYPE_STRING,
- 'description' => 'OAuth 2.0 application ID.',
+ 'description' => 'The name of the provider service.',
'default' => '',
- 'example' => '259125845563242502',
- ])
- ->addRule('secret', [
- 'type' => self::TYPE_STRING,
- 'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration.',
- 'default' => '',
- 'example' => 'Bpw_g9c2TGXxfgLshDbSaL8tsCcqgczQ',
+ 'example' => 'mailgun',
])
->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
- 'description' => 'Provider is active and can be used to create session.',
- 'example' => '',
+ 'description' => 'Is provider enabled?',
+ 'default' => true,
+ 'example' => true,
])
- ;
+ ->addRule('type', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Type of provider.',
+ 'default' => '',
+ 'example' => MESSAGE_TYPE_SMS,
+ ])
+ ->addRule('credentials', [
+ 'type' => self::TYPE_JSON,
+ 'description' => 'Provider credentials.',
+ 'default' => [],
+ 'example' => [
+ 'key' => '123456789'
+ ],
+ ])
+ ->addRule('options', [
+ 'type' => self::TYPE_JSON,
+ 'description' => 'Provider options.',
+ 'default' => [],
+ 'required' => false,
+ 'example' => [
+ 'from' => 'sender-email@mydomain'
+ ],
+ ]);
}
/**
diff --git a/src/Appwrite/Utopia/Response/Model/Subscriber.php b/src/Appwrite/Utopia/Response/Model/Subscriber.php
new file mode 100644
index 0000000000..8c3a4c7a49
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/Subscriber.php
@@ -0,0 +1,97 @@
+addRule('$id', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Subscriber ID.',
+ 'default' => '',
+ 'example' => '259125845563242502',
+ ])
+ ->addRule('$createdAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Subscriber creation time in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('$updatedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Subscriber update date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('targetId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Target ID.',
+ 'default' => '',
+ 'example' => '259125845563242502',
+ ])
+ ->addRule('target', [
+ 'type' => Response::MODEL_TARGET,
+ 'description' => 'Target.',
+ 'default' => [],
+ 'example' => [
+ '$id' => '259125845563242502',
+ '$createdAt' => self::TYPE_DATETIME_EXAMPLE,
+ '$updatedAt' => self::TYPE_DATETIME_EXAMPLE,
+ 'providerType' => 'email',
+ 'providerId' => '259125845563242502',
+ 'name' => 'ageon-app-email',
+ 'identifier' => 'random-mail@email.org',
+ 'userId' => '5e5ea5c16897e',
+ ],
+ ])
+ ->addRule('userId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Topic ID.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('userName', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'User Name.',
+ 'default' => '',
+ 'example' => 'Aegon Targaryen',
+ ])
+ ->addRule('topicId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Topic ID.',
+ 'default' => '',
+ 'example' => '259125845563242502',
+ ])
+ ->addRule('providerType', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.',
+ 'default' => '',
+ 'example' => MESSAGE_TYPE_EMAIL,
+ ]);
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'Subscriber';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_SUBSCRIBER;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Target.php b/src/Appwrite/Utopia/Response/Model/Target.php
new file mode 100644
index 0000000000..d180b6c4c4
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/Target.php
@@ -0,0 +1,83 @@
+addRule('$id', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Target ID.',
+ 'default' => '',
+ 'example' => '259125845563242502',
+ ])
+ ->addRule('$createdAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Target creation time in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('$updatedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Target update date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('name', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Target Name.',
+ 'default' => '',
+ 'example' => 'Aegon apple token',
+ ])
+ ->addRule('userId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'User ID.',
+ 'default' => '',
+ 'example' => '259125845563242502',
+ ])
+ ->addRule('providerId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Provider ID.',
+ 'required' => false,
+ 'default' => '',
+ 'example' => '259125845563242502',
+ ])
+ ->addRule('providerType', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.',
+ 'default' => '',
+ 'example' => MESSAGE_TYPE_EMAIL,
+ ])
+ ->addRule('identifier', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'The target identifier.',
+ 'default' => '',
+ 'example' => 'token',
+ ]);
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'Target';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_TARGET;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Topic.php b/src/Appwrite/Utopia/Response/Model/Topic.php
new file mode 100644
index 0000000000..096ddb347f
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/Topic.php
@@ -0,0 +1,71 @@
+addRule('$id', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Topic ID.',
+ 'default' => '',
+ 'example' => '259125845563242502',
+ ])
+ ->addRule('$createdAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Topic creation time in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('$updatedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Topic update date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('name', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'The name of the topic.',
+ 'default' => '',
+ 'example' => 'events',
+ ])
+ ->addRule('total', [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Total count of subscribers subscribed to topic.',
+ 'default' => 0,
+ 'example' => 100,
+ ])
+ ->addRule('description', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Description of the topic.',
+ 'default' => '',
+ 'required' => false,
+ 'example' => 'All events related messages will be sent to this topic.',
+ ]);
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'Topic';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_TOPIC;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/User.php b/src/Appwrite/Utopia/Response/Model/User.php
index ce2bd6188c..d6988b47f4 100644
--- a/src/Appwrite/Utopia/Response/Model/User.php
+++ b/src/Appwrite/Utopia/Response/Model/User.php
@@ -120,6 +120,13 @@ class User extends Model
'default' => new \stdClass(),
'example' => ['theme' => 'pink', 'timezone' => 'UTC'],
])
+ ->addRule('targets', [
+ 'type' => Response::MODEL_TARGET,
+ 'description' => 'A user-owned message receiver. A single user may have multiple e.g. emails, phones, and a browser. Each target is registered with a single provider.',
+ 'default' => [],
+ 'array' => true,
+ 'example' => [],
+ ])
->addRule('accessedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_USER_ACCCESS / 60 / 60 . ' hours.',
diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php
index 5f7bf85d0e..a84a91893c 100644
--- a/tests/e2e/Scopes/ProjectCustom.php
+++ b/tests/e2e/Scopes/ProjectCustom.php
@@ -82,7 +82,17 @@ trait ProjectCustom
'avatars.read',
'health.read',
'rules.read',
- 'rules.write'
+ 'rules.write',
+ 'targets.read',
+ 'targets.write',
+ 'providers.read',
+ 'providers.write',
+ 'messages.read',
+ 'messages.write',
+ 'topics.write',
+ 'topics.read',
+ 'subscribers.write',
+ 'subscribers.read',
],
]);
diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php
index e6f5feaa84..fe9983d9b8 100644
--- a/tests/e2e/Services/Account/AccountBase.php
+++ b/tests/e2e/Services/Account/AccountBase.php
@@ -94,6 +94,36 @@ trait AccountBase
$this->assertEquals($response['headers']['status-code'], 400);
+ $shortPassword = 'short';
+ $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]), [
+ 'userId' => ID::unique(),
+ 'email' => 'shortpass@appwrite.io',
+ 'password' => $shortPassword
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $longPassword = '';
+ for ($i = 0; $i < 257; $i++) { // 256 is the limit
+ $longPassword .= 'p';
+ }
+
+ $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]), [
+ 'userId' => ID::unique(),
+ 'email' => 'longpass@appwrite.io',
+ 'password' => $longPassword,
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
return [
'id' => $id,
'email' => $email,
diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php
index 56280b4b49..721a16b2b2 100644
--- a/tests/e2e/Services/Account/AccountCustomClientTest.php
+++ b/tests/e2e/Services/Account/AccountCustomClientTest.php
@@ -2,16 +2,16 @@
namespace Tests\E2E\Services\Account;
-use Appwrite\Extend\Exception;
-use Appwrite\SMS\Adapter\Mock;
use Appwrite\Tests\Retry;
use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
+use Utopia\App;
use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
+use Utopia\DSN\DSN;
use function sleep;
@@ -764,6 +764,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
$userId = $response['body']['userId'];
+ $messageId = $response['body']['$id'];
/**
* Test for FAILURE
diff --git a/tests/e2e/Services/GraphQL/AccountTest.php b/tests/e2e/Services/GraphQL/AccountTest.php
index 3985988d78..970d009bac 100644
--- a/tests/e2e/Services/GraphQL/AccountTest.php
+++ b/tests/e2e/Services/GraphQL/AccountTest.php
@@ -6,7 +6,9 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
+use Utopia\App;
use Utopia\Database\Helpers\ID;
+use Utopia\DSN\DSN;
class AccountTest extends Scope
{
@@ -123,6 +125,7 @@ class AccountTest extends Scope
public function testCreatePhoneVerification(): array
{
$projectId = $this->getProject()['$id'];
+
$query = $this->getQuery(self::$CREATE_PHONE_VERIFICATION);
$graphQLPayload = [
'query' => $query,
diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php
index f705e8d77b..2854d0bf42 100644
--- a/tests/e2e/Services/GraphQL/Base.php
+++ b/tests/e2e/Services/GraphQL/Base.php
@@ -109,6 +109,11 @@ trait Base
public static string $DELETE_USER_SESSIONS = 'delete_user_sessions';
public static string $DELETE_USER_SESSION = 'delete_user_session';
public static string $DELETE_USER = 'delete_user';
+ public static string $CREATE_USER_TARGET = 'create_user_target';
+ public static string $LIST_USER_TARGETS = 'list_user_targets';
+ public static string $GET_USER_TARGET = 'get_user_target';
+ public static string $UPDATE_USER_TARGET = 'update_user_target';
+ public static string $DELETE_USER_TARGET = 'delete_user_target';
// Teams
public static string $GET_TEAM = 'get_team';
@@ -199,6 +204,53 @@ trait Base
public static string $GET_QRCODE = 'get_qrcode';
public static string $GET_USER_INITIALS = 'get_user_initials';
+ // Providers
+ public static string $CREATE_MAILGUN_PROVIDER = 'create_mailgun_provider';
+ public static string $CREATE_SENDGRID_PROVIDER = 'create_sendgrid_provider';
+ public static string $CREATE_TWILIO_PROVIDER = 'create_twilio_provider';
+ public static string $CREATE_TELESIGN_PROVIDER = 'create_telesign_provider';
+ public static string $CREATE_TEXTMAGIC_PROVIDER = 'create_textmagic_provider';
+ public static string $CREATE_MSG91_PROVIDER = 'create_msg91_provider';
+ public static string $CREATE_VONAGE_PROVIDER = 'create_vonage_provider';
+ public static string $CREATE_FCM_PROVIDER = 'create_fcm_provider';
+ public static string $CREATE_APNS_PROVIDER = 'create_apns_provider';
+ public static string $LIST_PROVIDERS = 'list_providers';
+ public static string $GET_PROVIDER = 'get_provider';
+ public static string $UPDATE_MAILGUN_PROVIDER = 'update_mailgun_provider';
+ public static string $UPDATE_SENDGRID_PROVIDER = 'update_sendgrid_provider';
+ public static string $UPDATE_TWILIO_PROVIDER = 'update_twilio_provider';
+ public static string $UPDATE_TELESIGN_PROVIDER = 'update_telesign_provider';
+ public static string $UPDATE_TEXTMAGIC_PROVIDER = 'update_textmagic_provider';
+ public static string $UPDATE_MSG91_PROVIDER = 'update_msg91_provider';
+ public static string $UPDATE_VONAGE_PROVIDER = 'update_vonage_provider';
+ public static string $UPDATE_FCM_PROVIDER = 'update_fcm_provider';
+ public static string $UPDATE_APNS_PROVIDER = 'update_apns_provider';
+ public static string $DELETE_PROVIDER = 'delete_provider';
+
+ // Topics
+ public static string $CREATE_TOPIC = 'create_topic';
+ public static string $LIST_TOPICS = 'list_topics';
+ public static string $GET_TOPIC = 'get_topic';
+ public static string $UPDATE_TOPIC = 'update_topic';
+ public static string $DELETE_TOPIC = 'delete_topic';
+
+ // Subscriptions
+ public static string $CREATE_SUBSCRIBER = 'create_subscriber';
+ public static string $LIST_SUBSCRIBERS = 'list_subscribers';
+ public static string $GET_SUBSCRIBER = 'get_subscriber';
+ public static string $DELETE_SUBSCRIBER = 'delete_subscriber';
+
+ // Messages
+ public static string $CREATE_EMAIL = 'create_email';
+ public static string $CREATE_SMS = 'create_sms';
+ public static string $CREATE_PUSH_NOTIFICATION = 'create_push_notification';
+ public static string $LIST_MESSAGES = 'list_messages';
+ public static string $GET_MESSAGE = 'get_message';
+
+ public static string $UPDATE_EMAIL = 'update_email';
+ public static string $UPDATE_SMS = 'update_sms';
+ public static string $UPDATE_PUSH_NOTIFICATION = 'update_push_notification';
+
// Complex queries
public static string $COMPLEX_QUERY = 'complex_query';
@@ -881,6 +933,55 @@ trait Base
status
}
}';
+ case self::$CREATE_USER_TARGET:
+ return 'mutation createUserTarget($userId: String!, $targetId: String!, $providerType: String!, $identifier: String! $providerId: String){
+ usersCreateTarget(userId: $userId, targetId: $targetId, providerType: $providerType, identifier: $identifier, providerId: $providerId) {
+ _id
+ userId
+ providerType
+ providerId
+ identifier
+ }
+ }';
+ case self::$LIST_USER_TARGETS:
+ return 'query listUserTargets($userId: String!) {
+ usersListTargets(userId: $userId) {
+ total
+ targets {
+ _id
+ userId
+ providerType
+ providerId
+ identifier
+ }
+ }
+ }';
+ case self::$GET_USER_TARGET:
+ return 'query getUserTarget($userId: String!, $targetId: String!) {
+ usersGetTarget(userId: $userId, targetId: $targetId) {
+ _id
+ userId
+ providerType
+ providerId
+ identifier
+ }
+ }';
+ case self::$UPDATE_USER_TARGET:
+ return 'mutation updateUserTarget($userId: String!, $targetId: String!, $providerId: String, $identifier: String){
+ usersUpdateTarget(userId: $userId, targetId: $targetId, providerId: $providerId, identifier: $identifier) {
+ _id
+ userId
+ providerType
+ providerId
+ identifier
+ }
+ }';
+ case self::$DELETE_USER_TARGET:
+ return 'mutation deleteUserTarget($userId: String!, $targetId: String!){
+ usersDeleteTarget(userId: $userId, targetId: $targetId) {
+ status
+ }
+ }';
case self::$GET_LOCALE:
return 'query getLocale {
localeGet {
@@ -1688,6 +1789,439 @@ trait Base
status
}
}';
+ case self::$CREATE_MAILGUN_PROVIDER:
+ return 'mutation createMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $from: String!, $isEuRegion: Boolean!) {
+ messagingCreateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, from: $from, isEuRegion: $isEuRegion) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$CREATE_SENDGRID_PROVIDER:
+ return 'mutation createSendgridProvider($providerId: String!, $name: String!, $from: String!, $apiKey: String!) {
+ messagingCreateSendgridProvider(providerId: $providerId, name: $name, from: $from, apiKey: $apiKey) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$CREATE_TWILIO_PROVIDER:
+ return 'mutation createTwilioProvider($providerId: String!, $name: String!, $from: String!, $accountSid: String!, $authToken: String!) {
+ messagingCreateTwilioProvider(providerId: $providerId, name: $name, from: $from, accountSid: $accountSid, authToken: $authToken) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$CREATE_TELESIGN_PROVIDER:
+ return 'mutation createTelesignProvider($providerId: String!, $name: String!, $from: String!, $username: String!, $password: String!) {
+ messagingCreateTelesignProvider(providerId: $providerId, name: $name, from: $from, username: $username, password: $password) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$CREATE_TEXTMAGIC_PROVIDER:
+ return 'mutation createTextmagicProvider($providerId: String!, $name: String!, $from: String!, $username: String!, $apiKey: String!) {
+ messagingCreateTextmagicProvider(providerId: $providerId, name: $name, from: $from, username: $username, apiKey: $apiKey) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$CREATE_MSG91_PROVIDER:
+ return 'mutation createMsg91Provider($providerId: String!, $name: String!, $from: String!, $senderId: String!, $authKey: String!, $enabled: Boolean) {
+ messagingCreateMsg91Provider(providerId: $providerId, name: $name, from: $from, senderId: $senderId, authKey: $authKey, enabled: $enabled) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$CREATE_VONAGE_PROVIDER:
+ return 'mutation createVonageProvider($providerId: String!, $name: String!, $from: String!, $apiKey: String!, $apiSecret: String!) {
+ messagingCreateVonageProvider(providerId: $providerId, name: $name, from: $from, apiKey: $apiKey, apiSecret: $apiSecret) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$CREATE_FCM_PROVIDER:
+ return 'mutation createFcmProvider($providerId: String!, $name: String!, $serverKey: String!) {
+ messagingCreateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$CREATE_APNS_PROVIDER:
+ return 'mutation createApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) {
+ messagingCreateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$LIST_PROVIDERS:
+ return 'query listProviders {
+ messagingListProviders {
+ total
+ providers {
+ _id
+ name
+ provider
+ type
+
+ enabled
+ }
+ }
+ }';
+ case self::$GET_PROVIDER:
+ return 'query getProvider($providerId: String!) {
+ messagingGetProvider(providerId: $providerId) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$UPDATE_MAILGUN_PROVIDER:
+ return 'mutation updateMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $isEuRegion: Boolean, $enabled: Boolean) {
+ messagingUpdateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, isEuRegion: $isEuRegion, enabled: $enabled) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$UPDATE_SENDGRID_PROVIDER:
+ return 'mutation messagingUpdateSendgridProvider($providerId: String!, $name: String!, $apiKey: String!) {
+ messagingUpdateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$UPDATE_TWILIO_PROVIDER:
+ return 'mutation updateTwilioProvider($providerId: String!, $name: String!, $accountSid: String!, $authToken: String!) {
+ messagingUpdateTwilioProvider(providerId: $providerId, name: $name, accountSid: $accountSid, authToken: $authToken) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$UPDATE_TELESIGN_PROVIDER:
+ return 'mutation updateTelesignProvider($providerId: String!, $name: String!, $username: String!, $password: String!) {
+ messagingUpdateTelesignProvider(providerId: $providerId, name: $name, username: $username, password: $password) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$UPDATE_TEXTMAGIC_PROVIDER:
+ return 'mutation updateTextmagicProvider($providerId: String!, $name: String!, $username: String!, $apiKey: String!) {
+ messagingUpdateTextmagicProvider(providerId: $providerId, name: $name, username: $username, apiKey: $apiKey) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$UPDATE_MSG91_PROVIDER:
+ return 'mutation updateMsg91Provider($providerId: String!, $name: String!, $senderId: String!, $authKey: String!) {
+ messagingUpdateMsg91Provider(providerId: $providerId, name: $name, senderId: $senderId, authKey: $authKey) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$UPDATE_VONAGE_PROVIDER:
+ return 'mutation updateVonageProvider($providerId: String!, $name: String!, $apiKey: String!, $apiSecret: String!) {
+ messagingUpdateVonageProvider(providerId: $providerId, name: $name, apiKey: $apiKey, apiSecret: $apiSecret) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$UPDATE_FCM_PROVIDER:
+ return 'mutation updateFcmProvider($providerId: String!, $name: String!, $serverKey: String!) {
+ messagingUpdateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$UPDATE_APNS_PROVIDER:
+ return 'mutation updateApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) {
+ messagingUpdateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) {
+ _id
+ name
+ provider
+ type
+ enabled
+ }
+ }';
+ case self::$DELETE_PROVIDER:
+ return 'mutation deleteProvider($providerId: String!) {
+ messagingDeleteProvider(providerId: $providerId) {
+ status
+ }
+ }';
+ case self::$CREATE_TOPIC:
+ return 'mutation createTopic($topicId: String!, $name: String!, $description: String!) {
+ messagingCreateTopic(topicId: $topicId, name: $name, description: $description) {
+ _id
+ name
+ description
+ }
+ }';
+ case self::$LIST_TOPICS:
+ return 'query listTopics {
+ messagingListTopics {
+ total
+ topics {
+ _id
+ name
+ description
+ }
+ }
+ }';
+ case self::$GET_TOPIC:
+ return 'query getTopic($topicId: String!) {
+ messagingGetTopic(topicId: $topicId) {
+ _id
+ name
+ description
+ }
+ }';
+ case self::$UPDATE_TOPIC:
+ return 'mutation updateTopic($topicId: String!, $name: String!, $description: String!) {
+ messagingUpdateTopic(topicId: $topicId, name: $name, description: $description) {
+ _id
+ name
+ description
+ }
+ }';
+ case self::$DELETE_TOPIC:
+ return 'mutation deleteTopic($topicId: String!) {
+ messagingDeleteTopic(topicId: $topicId) {
+ status
+ }
+ }';
+ case self::$CREATE_SUBSCRIBER:
+ return 'mutation createSubscriber($subscriberId: String!, $targetId: String!, $topicId: String!) {
+ messagingCreateSubscriber(subscriberId: $subscriberId, targetId: $targetId, topicId: $topicId) {
+ _id
+ targetId
+ topicId
+ userName
+ target {
+ _id
+ userId
+ name
+ providerType
+ identifier
+ }
+ }
+ }';
+ case self::$LIST_SUBSCRIBERS:
+ return 'query listSubscribers($topicId: String!) {
+ messagingListSubscribers(topicId: $topicId) {
+ total
+ subscribers {
+ _id
+ targetId
+ topicId
+ userName
+ target {
+ _id
+ userId
+ name
+ providerType
+ identifier
+ }
+ }
+ }
+ }';
+ case self::$GET_SUBSCRIBER:
+ return 'query getSubscriber($topicId: String!, $subscriberId: String!) {
+ messagingGetSubscriber(topicId: $topicId, subscriberId: $subscriberId) {
+ _id
+ targetId
+ topicId
+ userName
+ target {
+ _id
+ userId
+ name
+ providerType
+ identifier
+ }
+ }
+ }';
+ case self::$DELETE_SUBSCRIBER:
+ return 'mutation deleteSubscriber($topicId: String!, $subscriberId: String!) {
+ messagingDeleteSubscriber(topicId: $topicId, subscriberId: $subscriberId) {
+ status
+ }
+ }';
+ case self::$CREATE_EMAIL:
+ return 'mutation createEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String!, $content: String!, $status: String, $description: String, $html: Boolean, $scheduledAt: String) {
+ messagingCreateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) {
+ _id
+ topics
+ users
+ targets
+ scheduledAt
+ deliveredAt
+ deliveryErrors
+ deliveredTotal
+ status
+ description
+ }
+ }';
+ case self::$CREATE_SMS:
+ return 'mutation createSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String!, $status: String, $description: String, $scheduledAt: String) {
+ messagingCreateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, scheduledAt: $scheduledAt) {
+ _id
+ topics
+ users
+ targets
+ scheduledAt
+ deliveredAt
+ deliveryErrors
+ deliveredTotal
+ status
+ description
+ }
+ }';
+ case self::$CREATE_PUSH_NOTIFICATION:
+ return 'mutation createPushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String!, $body: String!, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $scheduledAt: String) {
+ messagingCreatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, scheduledAt: $scheduledAt) {
+ _id
+ topics
+ users
+ targets
+ scheduledAt
+ deliveredAt
+ deliveryErrors
+ deliveredTotal
+ status
+ description
+ }
+ }';
+ case self::$LIST_MESSAGES:
+ return 'query listMessages {
+ messagingListMessages {
+ total
+ messages {
+ _id
+ providerType
+ topics
+ users
+ targets
+ scheduledAt
+ deliveredAt
+ deliveryErrors
+ deliveredTotal
+ status
+ description
+ }
+ }
+ }';
+ case self::$GET_MESSAGE:
+ return 'query getMessage($messageId: String!) {
+ messagingGetMessage(messageId: $messageId) {
+ _id
+ providerType
+ topics
+ users
+ targets
+ scheduledAt
+ deliveredAt
+ deliveryErrors
+ deliveredTotal
+ status
+ description
+ }
+ }';
+ case self::$UPDATE_EMAIL:
+ return 'mutation updateEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String, $content: String, $status: String, $description: String, $html: Boolean, $scheduledAt: String) {
+ messagingUpdateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) {
+ _id
+ topics
+ users
+ targets
+ scheduledAt
+ deliveredAt
+ deliveryErrors
+ deliveredTotal
+ status
+ description
+ }
+ }';
+ case self::$UPDATE_SMS:
+ return 'mutation updateSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String, $status: String, $description: String, $scheduledAt: String) {
+ messagingUpdateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, scheduledAt: $scheduledAt) {
+ _id
+ topics
+ users
+ targets
+ scheduledAt
+ deliveredAt
+ deliveryErrors
+ deliveredTotal
+ status
+ description
+ }
+ }';
+ case self::$UPDATE_PUSH_NOTIFICATION:
+ return 'mutation updatePushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String, $body: String, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $scheduledAt: String) {
+ messagingUpdatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, scheduledAt: $scheduledAt) {
+ _id
+ topics
+ users
+ targets
+ scheduledAt
+ deliveredAt
+ deliveryErrors
+ deliveredTotal
+ status
+ description
+ }
+ }';
case self::$COMPLEX_QUERY:
return 'mutation complex($databaseId: String!, $databaseName: String!, $collectionId: String!, $collectionName: String!, $documentSecurity: Boolean!, $collectionPermissions: [String!]!) {
databasesCreate(databaseId: $databaseId, name: $databaseName) {
@@ -1940,7 +2474,7 @@ trait Base
protected string $stdout = '';
protected string $stderr = '';
- protected function packageCode($folder)
+ protected function packageCode($folder): void
{
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
}
diff --git a/tests/e2e/Services/GraphQL/MessagingTest.php b/tests/e2e/Services/GraphQL/MessagingTest.php
new file mode 100644
index 0000000000..d1a084cfc1
--- /dev/null
+++ b/tests/e2e/Services/GraphQL/MessagingTest.php
@@ -0,0 +1,1153 @@
+ [
+ 'providerId' => ID::unique(),
+ 'name' => 'Sengrid1',
+ 'apiKey' => 'my-apikey',
+ 'from' => 'sender-email@my-domain.com',
+ ],
+ 'Mailgun' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Mailgun1',
+ 'apiKey' => 'my-apikey',
+ 'domain' => 'my-domain',
+ 'from' => 'sender-email@my-domain.com',
+ 'isEuRegion' => false,
+ ],
+ 'Twilio' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Twilio1',
+ 'accountSid' => 'my-accountSid',
+ 'authToken' => 'my-authToken',
+ 'from' => '+123456789',
+ ],
+ 'Telesign' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Telesign1',
+ 'username' => 'my-username',
+ 'password' => 'my-password',
+ 'from' => '+123456789',
+ ],
+ 'Textmagic' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Textmagic1',
+ 'username' => 'my-username',
+ 'apiKey' => 'my-apikey',
+ 'from' => '+123456789',
+ ],
+ 'Msg91' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Ms91-1',
+ 'senderId' => 'my-senderid',
+ 'authKey' => 'my-authkey',
+ 'from' => '+123456789'
+ ],
+ 'Vonage' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Vonage1',
+ 'apiKey' => 'my-apikey',
+ 'apiSecret' => 'my-apisecret',
+ 'from' => '+123456789',
+ ],
+ 'Fcm' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'FCM1',
+ 'serverKey' => 'my-serverkey',
+ ],
+ 'Apns' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'APNS1',
+ 'authKey' => 'my-authkey',
+ 'authKeyId' => 'my-authkeyid',
+ 'teamId' => 'my-teamid',
+ 'bundleId' => 'my-bundleid',
+ 'endpoint' => 'my-endpoint',
+ ],
+ ];
+
+ $providers = [];
+
+ foreach (\array_keys($providersParams) as $key) {
+ $query = $this->getQuery('create_' . \strtolower($key) . '_provider');
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => $providersParams[$key],
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+ \array_push($providers, $response['body']['data']['messagingCreate' . $key . 'Provider']);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($providersParams[$key]['name'], $response['body']['data']['messagingCreate' . $key . 'Provider']['name']);
+ }
+
+ return $providers;
+ }
+
+ /**
+ * @depends testCreateProviders
+ */
+ public function testUpdateProviders(array $providers): array
+ {
+ $providersParams = [
+ 'Sendgrid' => [
+ 'providerId' => $providers[0]['_id'],
+ 'name' => 'Sengrid2',
+ 'apiKey' => 'my-apikey',
+ ],
+ 'Mailgun' => [
+ 'providerId' => $providers[1]['_id'],
+ 'name' => 'Mailgun2',
+ 'apiKey' => 'my-apikey',
+ 'domain' => 'my-domain',
+ ],
+ 'Twilio' => [
+ 'providerId' => $providers[2]['_id'],
+ 'name' => 'Twilio2',
+ 'accountSid' => 'my-accountSid',
+ 'authToken' => 'my-authToken',
+ ],
+ 'Telesign' => [
+ 'providerId' => $providers[3]['_id'],
+ 'name' => 'Telesign2',
+ 'username' => 'my-username',
+ 'password' => 'my-password',
+ ],
+ 'Textmagic' => [
+ 'providerId' => $providers[4]['_id'],
+ 'name' => 'Textmagic2',
+ 'username' => 'my-username',
+ 'apiKey' => 'my-apikey',
+ ],
+ 'Msg91' => [
+ 'providerId' => $providers[5]['_id'],
+ 'name' => 'Ms91-2',
+ 'senderId' => 'my-senderid',
+ 'authKey' => 'my-authkey',
+ ],
+ 'Vonage' => [
+ 'providerId' => $providers[6]['_id'],
+ 'name' => 'Vonage2',
+ 'apiKey' => 'my-apikey',
+ 'apiSecret' => 'my-apisecret',
+ ],
+ 'Fcm' => [
+ 'providerId' => $providers[7]['_id'],
+ 'name' => 'FCM2',
+ 'serverKey' => 'my-serverkey',
+ ],
+ 'Apns' => [
+ 'providerId' => $providers[8]['_id'],
+ 'name' => 'APNS2',
+ 'authKey' => 'my-authkey',
+ 'authKeyId' => 'my-authkeyid',
+ 'teamId' => 'my-teamid',
+ 'bundleId' => 'my-bundleid',
+ 'endpoint' => 'my-endpoint',
+ ],
+ ];
+ foreach (\array_keys($providersParams) as $index => $key) {
+ $query = $this->getQuery('update_' . \strtolower($key) . '_provider');
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => $providersParams[$key],
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], $graphQLPayload);
+ $providers[$index] = $response['body']['data']['messagingUpdate' . $key . 'Provider'];
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($providersParams[$key]['name'], $response['body']['data']['messagingUpdate' . $key . 'Provider']['name']);
+ }
+
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'query' => $this->getQuery('update_mailgun_provider'),
+ 'variables' => [
+ 'providerId' => $providers[1]['_id'],
+ 'name' => 'Mailgun2',
+ 'apiKey' => 'my-apikey',
+ 'domain' => 'my-domain',
+ 'isEuRegion' => true,
+ 'enabled' => false,
+ ]
+ ]);
+ $providers[1] = $response['body']['data']['messagingUpdateMailgunProvider'];
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('Mailgun2', $response['body']['data']['messagingUpdateMailgunProvider']['name']);
+ $this->assertEquals(false, $response['body']['data']['messagingUpdateMailgunProvider']['enabled']);
+ return $providers;
+ }
+
+ /**
+ * @depends testUpdateProviders
+ */
+ public function testListProviders(array $providers)
+ {
+ $query = $this->getQuery(self::$LIST_PROVIDERS);
+ $graphQLPayload = [
+ 'query' => $query,
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], $graphQLPayload);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(\count($providers), \count($response['body']['data']['messagingListProviders']['providers']));
+ }
+
+ /**
+ * @depends testUpdateProviders
+ */
+ public function testGetProvider(array $providers)
+ {
+ $query = $this->getQuery(self::$GET_PROVIDER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'providerId' => $providers[0]['_id'],
+ ]
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], $graphQLPayload);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($providers[0]['name'], $response['body']['data']['messagingGetProvider']['name']);
+ }
+
+ /**
+ * @depends testUpdateProviders
+ */
+ public function testDeleteProvider(array $providers)
+ {
+ foreach ($providers as $provider) {
+ $query = $this->getQuery(self::$DELETE_PROVIDER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'providerId' => $provider['_id'],
+ ]
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], $graphQLPayload);
+ $this->assertEquals(204, $response['headers']['status-code']);
+ }
+ }
+
+ public function testCreateTopic()
+ {
+ $query = $this->getQuery(self::$CREATE_TOPIC);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'topicId' => ID::unique(),
+ 'name' => 'topic1',
+ 'description' => 'Active users',
+ ],
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('topic1', $response['body']['data']['messagingCreateTopic']['name']);
+ $this->assertEquals('Active users', $response['body']['data']['messagingCreateTopic']['description']);
+
+ return $response['body']['data']['messagingCreateTopic'];
+ }
+
+ /**
+ * @depends testCreateTopic
+ */
+ public function testUpdateTopic(array $topic)
+ {
+ $topicId = $topic['_id'];
+ $query = $this->getQuery(self::$UPDATE_TOPIC);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'topicId' => $topicId,
+ 'name' => 'topic2',
+ 'description' => 'Inactive users',
+ ],
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('topic2', $response['body']['data']['messagingUpdateTopic']['name']);
+ $this->assertEquals('Inactive users', $response['body']['data']['messagingUpdateTopic']['description']);
+
+ return $topicId;
+ }
+
+ /**
+ * @depends testCreateTopic
+ */
+ public function testListTopics()
+ {
+ $query = $this->getQuery(self::$LIST_TOPICS);
+ $graphQLPayload = [
+ 'query' => $query,
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(1, \count($response['body']['data']['messagingListTopics']['topics']));
+ }
+
+ /**
+ * @depends testUpdateTopic
+ */
+ public function testGetTopic(string $topicId)
+ {
+ $query = $this->getQuery(self::$GET_TOPIC);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'topicId' => $topicId,
+ ],
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('topic2', $response['body']['data']['messagingGetTopic']['name']);
+ $this->assertEquals('Inactive users', $response['body']['data']['messagingGetTopic']['description']);
+ }
+
+ /**
+ * @depends testCreateTopic
+ */
+ public function testCreateSubscriber(array $topic)
+ {
+ $topicId = $topic['_id'];
+
+ $userId = $this->getUser()['$id'];
+
+ $providerParam = [
+ 'sendgrid' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Sengrid1',
+ 'apiKey' => 'my-apikey',
+ 'from' => 'sender-email@my-domain.com',
+ ]
+ ];
+ $query = $this->getQuery(self::$CREATE_SENDGRID_PROVIDER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => $providerParam['sendgrid'],
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $providerId = $response['body']['data']['messagingCreateSendgridProvider']['_id'];
+
+ $query = $this->getQuery(self::$CREATE_USER_TARGET);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'email',
+ 'userId' => $userId,
+ 'providerId' => $providerId,
+ 'identifier' => 'random-email@mail.org',
+ ],
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($userId, $response['body']['data']['usersCreateTarget']['userId']);
+ $this->assertEquals('random-email@mail.org', $response['body']['data']['usersCreateTarget']['identifier']);
+
+ $targetId = $response['body']['data']['usersCreateTarget']['_id'];
+
+ $query = $this->getQuery(self::$CREATE_SUBSCRIBER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'subscriberId' => ID::unique(),
+ 'topicId' => $topicId,
+ 'targetId' => $targetId,
+ ],
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), $graphQLPayload);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['topicId'], $topicId);
+ $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['targetId'], $targetId);
+ $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['target']['userId'], $userId);
+
+ return $response['body']['data']['messagingCreateSubscriber'];
+ }
+
+ /**
+ * @depends testCreateSubscriber
+ */
+ public function testListSubscribers(array $subscriber)
+ {
+ $query = $this->getQuery(self::$LIST_SUBSCRIBERS);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'topicId' => $subscriber['topicId'],
+ ],
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($subscriber['topicId'], $response['body']['data']['messagingListSubscribers']['subscribers'][0]['topicId']);
+ $this->assertEquals($subscriber['targetId'], $response['body']['data']['messagingListSubscribers']['subscribers'][0]['targetId']);
+ $this->assertEquals($subscriber['target']['userId'], $response['body']['data']['messagingListSubscribers']['subscribers'][0]['target']['userId']);
+ $this->assertEquals(1, \count($response['body']['data']['messagingListSubscribers']['subscribers']));
+ }
+
+ /**
+ * @depends testCreateSubscriber
+ */
+ public function testGetSubscriber(array $subscriber)
+ {
+ $topicId = $subscriber['topicId'];
+ $subscriberId = $subscriber['_id'];
+
+ $query = $this->getQuery(self::$GET_SUBSCRIBER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'topicId' => $topicId,
+ 'subscriberId' => $subscriberId,
+ ],
+ ];
+
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($subscriberId, $response['body']['data']['messagingGetSubscriber']['_id']);
+ $this->assertEquals($topicId, $response['body']['data']['messagingGetSubscriber']['topicId']);
+ $this->assertEquals($subscriber['targetId'], $response['body']['data']['messagingGetSubscriber']['targetId']);
+ $this->assertEquals($subscriber['target']['userId'], $response['body']['data']['messagingGetSubscriber']['target']['userId']);
+ }
+
+ /**
+ * @depends testCreateSubscriber
+ */
+ public function testDeleteSubscriber(array $subscriber)
+ {
+ $topicId = $subscriber['topicId'];
+ $subscriberId = $subscriber['_id'];
+
+ $query = $this->getQuery(self::$DELETE_SUBSCRIBER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'topicId' => $topicId,
+ 'subscriberId' => $subscriberId,
+ ],
+ ];
+
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ ], $this->getHeaders()), $graphQLPayload);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ }
+
+ /**
+ * @depends testUpdateTopic
+ */
+ public function testDeleteTopic(string $topicId)
+ {
+ $query = $this->getQuery(self::$DELETE_TOPIC);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'topicId' => $topicId,
+ ],
+ ];
+ $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(204, $response['headers']['status-code']);
+ }
+
+ public function testSendEmail()
+ {
+ if (empty(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'))) {
+ $this->markTestSkipped('Email DSN not provided');
+ }
+
+ $emailDSN = new DSN(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'));
+ $to = $emailDSN->getParam('to');
+ $from = $emailDSN->getParam('from');
+ $isEuRegion = $emailDSN->getParam('isEuRegion');
+ $apiKey = $emailDSN->getPassword();
+ $domain = $emailDSN->getUser();
+
+ if (empty($to) || empty($from) || empty($apiKey) || empty($domain) || empty($isEuRegion)) {
+ $this->markTestSkipped('Email provider not configured');
+ }
+
+ $query = $this->getQuery(self::$CREATE_MAILGUN_PROVIDER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Mailgun1',
+ 'apiKey' => $apiKey,
+ 'domain' => $domain,
+ 'from' => $from,
+ 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN),
+ ],
+ ];
+ $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $provider['headers']['status-code']);
+
+ $providerId = $provider['body']['data']['messagingCreateMailgunProvider']['_id'];
+
+ $query = $this->getQuery(self::$CREATE_TOPIC);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'topicId' => ID::unique(),
+ 'name' => 'topic1',
+ 'description' => 'Active users',
+ ],
+ ];
+ $topic = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $topic['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_USER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'userId' => ID::unique(),
+ 'email' => 'random1-mail@mail.org',
+ 'password' => 'password',
+ 'name' => 'Messaging User',
+ ]
+ ];
+ $user = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $user['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_USER_TARGET);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'email',
+ 'userId' => $user['body']['data']['usersCreate']['_id'],
+ 'providerId' => $providerId,
+ 'identifier' => $to,
+ ],
+ ];
+ $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $target['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_SUBSCRIBER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'subscriberId' => ID::unique(),
+ 'topicId' => $topic['body']['data']['messagingCreateTopic']['_id'],
+ 'targetId' => $target['body']['data']['usersCreateTarget']['_id'],
+ ],
+ ];
+ $subscriber = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), $graphQLPayload);
+
+ $this->assertEquals(200, $subscriber['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_EMAIL);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => ID::unique(),
+ 'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']],
+ 'subject' => 'Khali beats Undertaker',
+ 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
+ ],
+ ];
+ $email = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $email['headers']['status-code']);
+
+ \sleep(5);
+
+ $query = $this->getQuery(self::$GET_MESSAGE);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => $email['body']['data']['messagingCreateEmail']['_id'],
+ ],
+ ];
+ $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors']));
+
+ return $message['body']['data']['messagingGetMessage'];
+ }
+
+ /**
+ * @depends testSendEmail
+ */
+ public function testUpdateEmail(array $email)
+ {
+ $query = $this->getQuery(self::$CREATE_EMAIL);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => ID::unique(),
+ 'status' => 'draft',
+ 'topics' => [$email['topics'][0]],
+ 'subject' => 'Khali beats Undertaker',
+ 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
+ ],
+ ];
+ $email = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $email['headers']['status-code']);
+
+ $query = $this->getQuery(self::$UPDATE_EMAIL);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => $email['body']['data']['messagingCreateEmail']['_id'],
+ 'status' => 'processing',
+ ],
+ ];
+ $email = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $email['headers']['status-code']);
+
+ \sleep(5);
+
+ $query = $this->getQuery(self::$GET_MESSAGE);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => $email['body']['data']['messagingUpdateEmail']['_id'],
+ ],
+ ];
+ $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors']));
+ }
+
+ public function testSendSMS()
+ {
+ if (empty(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'))) {
+ $this->markTestSkipped('SMS DSN not provided');
+ }
+
+ $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'));
+ $to = $smsDSN->getParam('to');
+ $from = $smsDSN->getParam('from');
+ $authKey = $smsDSN->getPassword();
+ $senderId = $smsDSN->getUser();
+
+ if (empty($to) || empty($from) || empty($senderId) || empty($authKey)) {
+ $this->markTestSkipped('SMS provider not configured');
+ }
+
+ $query = $this->getQuery(self::$CREATE_MSG91_PROVIDER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Msg91-1',
+ 'senderId' => $senderId,
+ 'authKey' => $authKey,
+ 'from' => $from,
+ ],
+ ];
+ $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $provider['headers']['status-code']);
+
+ $providerId = $provider['body']['data']['messagingCreateMsg91Provider']['_id'];
+
+ $query = $this->getQuery(self::$CREATE_TOPIC);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'topicId' => ID::unique(),
+ 'name' => 'topic1',
+ 'description' => 'Active users',
+ ],
+ ];
+ $topic = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $topic['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_USER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'userId' => ID::unique(),
+ 'email' => 'random3-email@mail.org',
+ 'password' => 'password',
+ 'name' => 'Messaging User',
+ ]
+ ];
+ $user = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $user['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_USER_TARGET);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'sms',
+ 'userId' => $user['body']['data']['usersCreate']['_id'],
+ 'providerId' => $providerId,
+ 'identifier' => $to,
+ ],
+ ];
+ $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $target['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_SUBSCRIBER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'subscriberId' => ID::unique(),
+ 'topicId' => $topic['body']['data']['messagingCreateTopic']['_id'],
+ 'targetId' => $target['body']['data']['usersCreateTarget']['_id'],
+ ],
+ ];
+ $subscriber = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), $graphQLPayload);
+
+ $this->assertEquals(200, $subscriber['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_SMS);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => ID::unique(),
+ 'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']],
+ 'content' => '454665',
+ ],
+ ];
+ $sms = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $sms['headers']['status-code']);
+
+ \sleep(5);
+
+ $query = $this->getQuery(self::$GET_MESSAGE);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => $sms['body']['data']['messagingCreateSMS']['_id'],
+ ],
+ ];
+ $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors']));
+ return $message['body']['data']['messagingGetMessage'];
+ }
+
+ /**
+ * @depends testSendSMS
+ */
+ public function testUpdateSMS(array $sms)
+ {
+ $query = $this->getQuery(self::$CREATE_SMS);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => ID::unique(),
+ 'status' => 'draft',
+ 'topics' => [$sms['topics'][0]],
+ 'content' => '345463',
+ ],
+ ];
+ $sms = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $sms['headers']['status-code']);
+
+ $query = $this->getQuery(self::$UPDATE_SMS);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => $sms['body']['data']['messagingCreateSMS']['_id'],
+ 'status' => 'processing',
+ ],
+ ];
+ $sms = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $sms['headers']['status-code']);
+
+ \sleep(5);
+
+ $query = $this->getQuery(self::$GET_MESSAGE);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => $sms['body']['data']['messagingUpdateSMS']['_id'],
+ ],
+ ];
+ $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors']));
+ }
+
+ public function testSendPushNotification()
+ {
+ if (empty(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'))) {
+ $this->markTestSkipped('Push DSN empty');
+ }
+
+ $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'));
+ $to = $pushDSN->getParam('to');
+ $serverKey = $pushDSN->getPassword();
+
+ if (empty($to) || empty($serverKey)) {
+ $this->markTestSkipped('Push provider not configured');
+ }
+
+ $query = $this->getQuery(self::$CREATE_FCM_PROVIDER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'FCM1',
+ 'serverKey' => $serverKey,
+ ],
+ ];
+ $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $provider['headers']['status-code']);
+
+ $providerId = $provider['body']['data']['messagingCreateFcmProvider']['_id'];
+
+ $query = $this->getQuery(self::$CREATE_TOPIC);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'topicId' => ID::unique(),
+ 'name' => 'topic1',
+ 'description' => 'Active users',
+ ],
+ ];
+ $topic = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $topic['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_USER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'userId' => ID::unique(),
+ 'email' => 'random5-mail@mail.org',
+ 'password' => 'password',
+ 'name' => 'Messaging User',
+ ]
+ ];
+ $user = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $user['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_USER_TARGET);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'push',
+ 'userId' => $user['body']['data']['usersCreate']['_id'],
+ 'providerId' => $providerId,
+ 'identifier' => $to,
+ ],
+ ];
+ $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $target['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_SUBSCRIBER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'subscriberId' => ID::unique(),
+ 'topicId' => $topic['body']['data']['messagingCreateTopic']['_id'],
+ 'targetId' => $target['body']['data']['usersCreateTarget']['_id'],
+ ],
+ ];
+ $subscriber = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), $graphQLPayload);
+
+ $this->assertEquals(200, $subscriber['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_PUSH_NOTIFICATION);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => ID::unique(),
+ 'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']],
+ 'title' => 'Push Notification Title',
+ 'body' => 'Push Notifiaction Body',
+ ],
+ ];
+ $push = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $push['headers']['status-code']);
+
+ \sleep(5);
+
+ $query = $this->getQuery(self::$GET_MESSAGE);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => $push['body']['data']['messagingCreatePushNotification']['_id'],
+ ],
+ ];
+ $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors']));
+
+ return $message['body']['data']['messagingGetMessage'];
+ }
+
+ /**
+ * @depends testSendPushNotification
+ */
+ public function testUpdatePushNotification(array $push)
+ {
+ $query = $this->getQuery(self::$CREATE_PUSH_NOTIFICATION);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => ID::unique(),
+ 'status' => 'draft',
+ 'topics' => [$push['topics'][0]],
+ 'title' => 'Push Notification Title',
+ 'body' => 'Push Notifiaction Body',
+ ],
+ ];
+ $push = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $push['headers']['status-code']);
+
+ $query = $this->getQuery(self::$UPDATE_PUSH_NOTIFICATION);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => $push['body']['data']['messagingCreatePushNotification']['_id'],
+ 'status' => 'processing',
+ ],
+ ];
+ $push = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $push['headers']['status-code']);
+
+ \sleep(5);
+
+ $query = $this->getQuery(self::$GET_MESSAGE);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'messageId' => $push['body']['data']['messagingUpdatePushNotification']['_id'],
+ ],
+ ];
+ $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $graphQLPayload);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors']));
+ }
+}
diff --git a/tests/e2e/Services/GraphQL/UsersTest.php b/tests/e2e/Services/GraphQL/UsersTest.php
index 9bd503df0f..1dac9123a9 100644
--- a/tests/e2e/Services/GraphQL/UsersTest.php
+++ b/tests/e2e/Services/GraphQL/UsersTest.php
@@ -45,6 +45,56 @@ class UsersTest extends Scope
return $user;
}
+ /**
+ * @depends testCreateUser
+ */
+ public function testCreateUserTarget(array $user)
+ {
+ $projectId = $this->getProject()['$id'];
+
+ $query = $this->getQuery(self::$CREATE_MAILGUN_PROVIDER);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Mailgun1',
+ 'apiKey' => 'api-key',
+ 'domain' => 'domain',
+ 'from' => 'from@domain.com',
+ 'isEuRegion' => false,
+ ],
+ ];
+ $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), $graphQLPayload);
+ $providerId = $provider['body']['data']['messagingCreateMailgunProvider']['_id'];
+
+ $this->assertEquals(200, $provider['headers']['status-code']);
+
+ $query = $this->getQuery(self::$CREATE_USER_TARGET);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'targetId' => ID::unique(),
+ 'userId' => $user['_id'],
+ 'providerType' => 'email',
+ 'providerId' => $providerId,
+ 'identifier' => 'random-email@mail.org',
+ ]
+ ];
+
+ $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), $graphQLPayload);
+
+ $this->assertEquals(200, $target['headers']['status-code']);
+ $this->assertEquals('random-email@mail.org', $target['body']['data']['usersCreateTarget']['identifier']);
+
+ return $target['body']['data']['usersCreateTarget'];
+ }
+
public function testGetUsers()
{
$projectId = $this->getProject()['$id'];
@@ -176,6 +226,54 @@ class UsersTest extends Scope
$this->assertIsArray($user['body']['data']['usersListLogs']);
}
+ /**
+ * @depends testCreateUserTarget
+ */
+ public function testListUserTargets(array $target)
+ {
+ $projectId = $this->getProject()['$id'];
+ $query = $this->getQuery(self::$LIST_USER_TARGETS);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'userId' => $target['userId'],
+ ]
+ ];
+
+ $targets = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), $graphQLPayload);
+
+ $this->assertEquals(200, $targets['headers']['status-code']);
+ $this->assertIsArray($targets['body']['data']['usersListTargets']);
+ $this->assertCount(2, $targets['body']['data']['usersListTargets']['targets']);
+ }
+
+ /**
+ * @depends testCreateUserTarget
+ */
+ public function testGetUserTarget(array $target)
+ {
+ $projectId = $this->getProject()['$id'];
+ $query = $this->getQuery(self::$GET_USER_TARGET);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'userId' => $target['userId'],
+ 'targetId' => $target['_id'],
+ ]
+ ];
+
+ $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), $graphQLPayload);
+
+ $this->assertEquals(200, $target['headers']['status-code']);
+ $this->assertEquals('random-email@mail.org', $target['body']['data']['usersGetTarget']['identifier']);
+ }
+
public function testUpdateUserStatus()
{
$projectId = $this->getProject()['$id'];
@@ -360,6 +458,31 @@ class UsersTest extends Scope
$this->assertEquals('{"key":"value"}', $user['body']['data']['usersUpdatePrefs']['data']);
}
+ /**
+ * @depends testCreateUserTarget
+ */
+ public function testUpdateUserTarget(array $target)
+ {
+ $projectId = $this->getProject()['$id'];
+ $query = $this->getQuery(self::$UPDATE_USER_TARGET);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'userId' => $target['userId'],
+ 'targetId' => $target['_id'],
+ 'identifier' => 'random-email1@mail.org',
+ ],
+ ];
+
+ $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), $graphQLPayload);
+
+ $this->assertEquals(200, $target['headers']['status-code']);
+ $this->assertEquals('random-email1@mail.org', $target['body']['data']['usersUpdateTarget']['identifier']);
+ }
+
public function testDeleteUserSessions()
{
$projectId = $this->getProject()['$id'];
@@ -407,6 +530,29 @@ class UsersTest extends Scope
$this->getUser();
}
+ /**
+ * @depends testCreateUserTarget
+ */
+ public function testDeleteUserTarget(array $target)
+ {
+ $projectId = $this->getProject()['$id'];
+ $query = $this->getQuery(self::$DELETE_USER_TARGET);
+ $graphQLPayload = [
+ 'query' => $query,
+ 'variables' => [
+ 'userId' => $target['userId'],
+ 'targetId' => $target['_id'],
+ ]
+ ];
+
+ $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ ], $this->getHeaders()), $graphQLPayload);
+
+ $this->assertEquals(204, $target['headers']['status-code']);
+ }
+
public function testDeleteUser()
{
$projectId = $this->getProject()['$id'];
diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php
new file mode 100644
index 0000000000..690e503e77
--- /dev/null
+++ b/tests/e2e/Services/Messaging/MessagingBase.php
@@ -0,0 +1,1040 @@
+ [
+ 'providerId' => ID::unique(),
+ 'name' => 'Sengrid1',
+ 'apiKey' => 'my-apikey',
+ 'from' => 'sender-email@my-domain.com',
+ ],
+ 'mailgun' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Mailgun1',
+ 'apiKey' => 'my-apikey',
+ 'domain' => 'my-domain',
+ 'from' => 'sender-email@my-domain.com',
+ 'isEuRegion' => false,
+ ],
+ 'twilio' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Twilio1',
+ 'accountSid' => 'my-accountSid',
+ 'authToken' => 'my-authToken',
+ 'from' => '+123456789',
+ ],
+ 'telesign' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Telesign1',
+ 'username' => 'my-username',
+ 'password' => 'my-password',
+ 'from' => '+123456789',
+ ],
+ 'textmagic' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Textmagic1',
+ 'username' => 'my-username',
+ 'apiKey' => 'my-apikey',
+ 'from' => '+123456789',
+ ],
+ 'msg91' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Ms91-1',
+ 'senderId' => 'my-senderid',
+ 'authKey' => 'my-authkey',
+ 'from' => '+123456789'
+ ],
+ 'vonage' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'Vonage1',
+ 'apiKey' => 'my-apikey',
+ 'apiSecret' => 'my-apisecret',
+ 'from' => '+123456789',
+ ],
+ 'fcm' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'FCM1',
+ 'serverKey' => 'my-serverkey',
+ ],
+ 'apns' => [
+ 'providerId' => ID::unique(),
+ 'name' => 'APNS1',
+ 'authKey' => 'my-authkey',
+ 'authKeyId' => 'my-authkeyid',
+ 'teamId' => 'my-teamid',
+ 'bundleId' => 'my-bundleid',
+ 'endpoint' => 'my-endpoint',
+ ],
+ ];
+ $providers = [];
+
+ foreach (\array_keys($providersParams) as $key) {
+ $response = $this->client->call(Client::METHOD_POST, '/messaging/providers/' . $key, \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), $providersParams[$key]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertEquals($providersParams[$key]['name'], $response['body']['name']);
+ \array_push($providers, $response['body']);
+ }
+
+ return $providers;
+ }
+
+ /**
+ * @depends testCreateProviders
+ */
+ public function testUpdateProviders(array $providers): array
+ {
+ $providersParams = [
+ 'sendgrid' => [
+ 'name' => 'Sengrid2',
+ 'apiKey' => 'my-apikey',
+ ],
+ 'mailgun' => [
+ 'name' => 'Mailgun2',
+ 'apiKey' => 'my-apikey',
+ 'domain' => 'my-domain',
+ ],
+ 'twilio' => [
+ 'name' => 'Twilio2',
+ 'accountSid' => 'my-accountSid',
+ 'authToken' => 'my-authToken',
+ ],
+ 'telesign' => [
+ 'name' => 'Telesign2',
+ 'username' => 'my-username',
+ 'password' => 'my-password',
+ ],
+ 'textmagic' => [
+ 'name' => 'Textmagic2',
+ 'username' => 'my-username',
+ 'apiKey' => 'my-apikey',
+ ],
+ 'msg91' => [
+ 'name' => 'Ms91-2',
+ 'senderId' => 'my-senderid',
+ 'authKey' => 'my-authkey',
+ ],
+ 'vonage' => [
+ 'name' => 'Vonage2',
+ 'apiKey' => 'my-apikey',
+ 'apiSecret' => 'my-apisecret',
+ ],
+ 'fcm' => [
+ 'name' => 'FCM2',
+ 'serverKey' => 'my-serverkey',
+ ],
+ 'apns' => [
+ 'name' => 'APNS2',
+ 'authKey' => 'my-authkey',
+ 'authKeyId' => 'my-authkeyid',
+ 'teamId' => 'my-teamid',
+ 'bundleId' => 'my-bundleid',
+ 'endpoint' => 'my-endpoint',
+ ],
+ ];
+ foreach (\array_keys($providersParams) as $index => $key) {
+ $response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/' . $key . '/' . $providers[$index]['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], $providersParams[$key]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($providersParams[$key]['name'], $response['body']['name']);
+ $providers[$index] = $response['body'];
+ }
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/mailgun/' . $providers[1]['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'name' => 'Mailgun2',
+ 'apiKey' => 'my-apikey',
+ 'domain' => 'my-domain',
+ 'isEuRegion' => true,
+ 'enabled' => false,
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('Mailgun2', $response['body']['name']);
+ $this->assertEquals(false, $response['body']['enabled']);
+ $providers[1] = $response['body'];
+ return $providers;
+ }
+
+ /**
+ * @depends testUpdateProviders
+ */
+ public function testListProviders(array $providers)
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(\count($providers), \count($response['body']['providers']));
+
+ return $providers;
+ }
+
+ /**
+ * @depends testUpdateProviders
+ */
+ public function testGetProvider(array $providers)
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $providers[0]['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($providers[0]['name'], $response['body']['name']);
+ }
+
+ /**
+ * @depends testUpdateProviders
+ */
+ public function testDeleteProvider(array $providers)
+ {
+ foreach ($providers as $provider) {
+ $response = $this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $provider['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals(204, $response['headers']['status-code']);
+ }
+ }
+
+ public function testCreateTopic(): array
+ {
+ $response = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'topicId' => ID::unique(),
+ 'name' => 'my-app',
+ 'description' => 'web app'
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertEquals('my-app', $response['body']['name']);
+
+ return $response['body'];
+ }
+
+ /**
+ * @depends testCreateTopic
+ */
+ public function testUpdateTopic(array $topic): string
+ {
+ $response = $this->client->call(Client::METHOD_PATCH, '/messaging/topics/' . $topic['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'name' => 'android-app',
+ 'description' => 'updated-description'
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('android-app', $response['body']['name']);
+ $this->assertEquals('updated-description', $response['body']['description']);
+ return $response['body']['$id'];
+ }
+
+ /**
+ * @depends testUpdateTopic
+ */
+ public function testListTopic(string $topicId)
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/topics', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'search' => 'updated-description',
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(1, \count($response['body']['topics']));
+
+ return $topicId;
+ }
+
+ /**
+ * @depends testUpdateTopic
+ */
+ public function testGetTopic(string $topicId)
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topicId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('android-app', $response['body']['name']);
+ $this->assertEquals('updated-description', $response['body']['description']);
+ $this->assertEquals(0, $response['body']['total']);
+ }
+
+ /**
+ * @depends testCreateTopic
+ */
+ public function testCreateSubscriber(array $topic)
+ {
+ $userId = $this->getUser()['$id'];
+
+ $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'providerId' => ID::unique(),
+ 'name' => 'Sendgrid1',
+ 'apiKey' => 'my-apikey',
+ 'from' => 'sender-email@my-domain.com',
+ ]);
+
+ $this->assertEquals(201, $provider['headers']['status-code']);
+
+ $target = $this->client->call(Client::METHOD_POST, '/users/' . $userId . '/targets', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'email',
+ 'providerId' => $provider['body']['$id'],
+ 'identifier' => 'random-email@mail.org',
+ ]);
+
+ $this->assertEquals(201, $target['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['$id'] . '/subscribers', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'subscriberId' => ID::unique(),
+ 'targetId' => $target['body']['$id'],
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertEquals($target['body']['userId'], $response['body']['target']['userId']);
+ $this->assertEquals($target['body']['providerType'], $response['body']['target']['providerType']);
+
+ $topic = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $topic['headers']['status-code']);
+ $this->assertEquals('android-app', $topic['body']['name']);
+ $this->assertEquals('updated-description', $topic['body']['description']);
+ $this->assertEquals(1, $topic['body']['total']);
+
+ return [
+ 'topicId' => $topic['body']['$id'],
+ 'targetId' => $target['body']['$id'],
+ 'userId' => $target['body']['userId'],
+ 'subscriberId' => $response['body']['$id'],
+ 'identifier' => $target['body']['identifier'],
+ 'providerType' => $target['body']['providerType'],
+ ];
+ }
+
+ /**
+ * @depends testCreateSubscriber
+ */
+ public function testGetSubscriber(array $data)
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $data['topicId'] . '/subscribers/' . $data['subscriberId'], \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($data['topicId'], $response['body']['topicId']);
+ $this->assertEquals($data['targetId'], $response['body']['targetId']);
+ $this->assertEquals($data['userId'], $response['body']['target']['userId']);
+ $this->assertEquals($data['providerType'], $response['body']['target']['providerType']);
+ $this->assertEquals($data['identifier'], $response['body']['target']['identifier']);
+ }
+
+ /**
+ * @depends testCreateSubscriber
+ */
+ public function testListSubscribers(array $data)
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $data['topicId'] . '/subscribers', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(1, $response['body']['total']);
+ $this->assertEquals($data['userId'], $response['body']['subscribers'][0]['target']['userId']);
+ $this->assertEquals($data['providerType'], $response['body']['subscribers'][0]['target']['providerType']);
+ $this->assertEquals($data['identifier'], $response['body']['subscribers'][0]['target']['identifier']);
+ $this->assertEquals(\count($response['body']['subscribers']), $response['body']['total']);
+
+ return $data;
+ }
+
+ /**
+ * @depends testListSubscribers
+ */
+ public function testGetSubscriberLogs(array $data): void
+ {
+ /**
+ * Test for SUCCESS
+ */
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'queries' => ['limit(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'queries' => ['offset(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'queries' => ['limit(1)', 'offset(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+
+ /**
+ * Test for FAILURE
+ */
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'queries' => ['limit(-1)']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'queries' => ['offset(-1)']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'queries' => ['equal("$id", "asdf")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'queries' => ['orderAsc("$id")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'queries' => ['cursorAsc("$id")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+ }
+
+ /**
+ * @depends testCreateSubscriber
+ */
+ public function testDeleteSubscriber(array $data)
+ {
+ $response = $this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $data['topicId'] . '/subscribers/' . $data['subscriberId'], \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(204, $response['headers']['status-code']);
+
+ $topic = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $data['topicId'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $topic['headers']['status-code']);
+ $this->assertEquals('android-app', $topic['body']['name']);
+ $this->assertEquals('updated-description', $topic['body']['description']);
+ $this->assertEquals(0, $topic['body']['total']);
+ }
+
+ /**
+ * @depends testUpdateTopic
+ */
+ public function testDeleteTopic(string $topicId)
+ {
+ $response = $this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals(204, $response['headers']['status-code']);
+ }
+
+ public function testSendEmail()
+ {
+ if (empty(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'))) {
+ $this->markTestSkipped('Email DSN not provided');
+ }
+
+ $emailDSN = new DSN(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'));
+ $to = $emailDSN->getParam('to');
+ $from = $emailDSN->getParam('from');
+ $isEuRegion = $emailDSN->getParam('isEuRegion');
+ $apiKey = $emailDSN->getPassword();
+ $domain = $emailDSN->getUser();
+
+ if (empty($to) || empty($from) || empty($apiKey) || empty($domain) || empty($isEuRegion)) {
+ $this->markTestSkipped('Email provider not configured');
+ }
+
+ // Create provider
+ $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/mailgun', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'providerId' => ID::unique(),
+ 'name' => 'Mailgun-provider',
+ 'apiKey' => $apiKey,
+ 'domain' => $domain,
+ 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN),
+ 'from' => $from
+ ]);
+
+ $this->assertEquals(201, $provider['headers']['status-code']);
+
+ // Create Topic
+ $topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'topicId' => ID::unique(),
+ 'name' => 'topic1',
+ 'description' => 'Test Topic'
+ ]);
+
+ $this->assertEquals(201, $topic['headers']['status-code']);
+
+ // Create User
+ $user = $this->client->call(Client::METHOD_POST, '/users', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'userId' => ID::unique(),
+ 'email' => $to,
+ 'password' => 'password',
+ 'name' => 'Messaging User',
+ ]);
+
+ $this->assertEquals(201, $user['headers']['status-code']);
+
+ // Create Target
+ $target = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'email',
+ 'providerId' => $provider['body']['$id'],
+ 'identifier' => $to,
+ ]);
+
+ $this->assertEquals(201, $target['headers']['status-code']);
+
+ // Create Subscriber
+ $subscriber = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['body']['$id'] . '/subscribers', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'subscriberId' => ID::unique(),
+ 'targetId' => $target['body']['$id'],
+ ]);
+
+ $this->assertEquals(201, $subscriber['headers']['status-code']);
+
+ // Create Email
+ $email = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'messageId' => ID::unique(),
+ 'topics' => [$topic['body']['$id']],
+ 'subject' => 'Khali beats Undertaker',
+ 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
+ ]);
+
+ $this->assertEquals(201, $email['headers']['status-code']);
+
+ \sleep(5);
+
+ $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'], [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['deliveryErrors']));
+
+ return $message;
+ }
+
+ /**
+ * @depends testSendEmail
+ */
+ public function testUpdateEmail(array $email)
+ {
+ $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ // Test failure as the message has already been sent.
+ $this->assertEquals(400, $message['headers']['status-code']);
+
+ // Create Email
+ $email = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'messageId' => ID::unique(),
+ 'status' => 'draft',
+ 'topics' => [$email['body']['topics'][0]],
+ 'subject' => 'Khali beats Undertaker',
+ 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
+ ]);
+
+ $this->assertEquals(201, $email['headers']['status-code']);
+
+ $email = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'status' => 'processing',
+ ]);
+
+ $this->assertEquals(200, $email['headers']['status-code']);
+
+ \sleep(5);
+
+ $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'], [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['deliveryErrors']));
+ }
+
+ public function testSendSMS()
+ {
+ if (empty(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'))) {
+ $this->markTestSkipped('SMS DSN not provided');
+ }
+
+ $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'));
+ $to = $smsDSN->getParam('to');
+ $from = $smsDSN->getParam('from');
+ $authKey = $smsDSN->getPassword();
+ $senderId = $smsDSN->getUser();
+
+ if (empty($to) || empty($from) || empty($senderId) || empty($authKey)) {
+ $this->markTestSkipped('SMS provider not configured');
+ }
+
+ // Create provider
+ $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/msg91', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'providerId' => ID::unique(),
+ 'name' => 'Msg91-1',
+ 'senderId' => $senderId,
+ 'authKey' => $authKey,
+ 'from' => $from
+ ]);
+
+ $this->assertEquals(201, $provider['headers']['status-code']);
+
+ // Create Topic
+ $topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'topicId' => ID::unique(),
+ 'name' => 'topic1',
+ 'description' => 'Test Topic'
+ ]);
+
+ $this->assertEquals(201, $topic['headers']['status-code']);
+
+ // Create User
+ $user = $this->client->call(Client::METHOD_POST, '/users', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'userId' => ID::unique(),
+ 'email' => 'random1-email@mail.org',
+ 'password' => 'password',
+ 'name' => 'Messaging User',
+ ]);
+
+ $this->assertEquals(201, $user['headers']['status-code']);
+
+ // Create Target
+ $target = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'sms',
+ 'providerId' => $provider['body']['$id'],
+ 'identifier' => $to,
+ ]);
+
+ $this->assertEquals(201, $target['headers']['status-code']);
+
+ // Create Subscriber
+ $subscriber = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['body']['$id'] . '/subscribers', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'subscriberId' => ID::unique(),
+ 'targetId' => $target['body']['$id'],
+ ]);
+
+ $this->assertEquals(201, $subscriber['headers']['status-code']);
+
+ // Create SMS
+ $sms = $this->client->call(Client::METHOD_POST, '/messaging/messages/sms', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'messageId' => ID::unique(),
+ 'topics' => [$topic['body']['$id']],
+ 'content' => '064763',
+ ]);
+
+ $this->assertEquals(201, $sms['headers']['status-code']);
+
+ \sleep(5);
+
+ $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $sms['body']['$id'], [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['deliveryErrors']));
+
+ return $message;
+ }
+
+ /**
+ * @depends testSendSMS
+ */
+ public function testUpdateSMS(array $sms)
+ {
+ $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/sms/' . $sms['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ // Test failure as the message has already been sent.
+ $this->assertEquals(400, $message['headers']['status-code']);
+
+ // Create SMS
+ $sms = $this->client->call(Client::METHOD_POST, '/messaging/messages/sms', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'messageId' => ID::unique(),
+ 'status' => 'draft',
+ 'topics' => [$sms['body']['topics'][0]],
+ 'content' => '047487',
+ ]);
+
+ $this->assertEquals(201, $sms['headers']['status-code']);
+
+ $sms = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/sms/' . $sms['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'status' => 'processing',
+ ]);
+
+ $this->assertEquals(200, $sms['headers']['status-code']);
+
+ \sleep(5);
+
+ $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $sms['body']['$id'], [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['deliveryErrors']));
+ }
+
+ public function testSendPushNotification()
+ {
+ if (empty(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'))) {
+ $this->markTestSkipped('Push DSN empty');
+ }
+
+ $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'));
+ $to = $pushDSN->getParam('to');
+ $serverKey = $pushDSN->getPassword();
+
+ if (empty($to) || empty($serverKey)) {
+ $this->markTestSkipped('Push provider not configured');
+ }
+
+ // Create provider
+ $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/fcm', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'providerId' => ID::unique(),
+ 'name' => 'FCM-1',
+ 'serverKey' => $serverKey,
+ ]);
+
+ $this->assertEquals(201, $provider['headers']['status-code']);
+
+ // Create Topic
+ $topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'topicId' => ID::unique(),
+ 'name' => 'topic1',
+ 'description' => 'Test Topic'
+ ]);
+
+ $this->assertEquals(201, $topic['headers']['status-code']);
+
+ // Create User
+ $user = $this->client->call(Client::METHOD_POST, '/users', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'userId' => ID::unique(),
+ 'email' => 'random3-email@mail.org',
+ 'password' => 'password',
+ 'name' => 'Messaging User',
+ ]);
+
+ $this->assertEquals(201, $user['headers']['status-code']);
+
+ // Create Target
+ $target = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'push',
+ 'providerId' => $provider['body']['$id'],
+ 'identifier' => $to,
+ ]);
+
+ $this->assertEquals(201, $target['headers']['status-code']);
+
+ // Create Subscriber
+ $subscriber = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['body']['$id'] . '/subscribers', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'subscriberId' => ID::unique(),
+ 'targetId' => $target['body']['$id'],
+ ]);
+
+ $this->assertEquals(201, $subscriber['headers']['status-code']);
+
+ // Create push notification
+ $push = $this->client->call(Client::METHOD_POST, '/messaging/messages/push', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'messageId' => ID::unique(),
+ 'topics' => [$topic['body']['$id']],
+ 'title' => 'Test-Notification',
+ 'body' => 'Test-Notification-Body',
+ ]);
+
+ $this->assertEquals(201, $push['headers']['status-code']);
+
+ \sleep(5);
+
+ $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $push['body']['$id'], [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['deliveryErrors']));
+
+ return $message;
+ }
+
+ /**
+ * @depends testSendPushNotification
+ */
+ public function testUpdatePushNotification(array $push)
+ {
+ $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/push/' . $push['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ // Test failure as the message has already been sent.
+ $this->assertEquals(400, $message['headers']['status-code']);
+
+ // Create push notification
+ $push = $this->client->call(Client::METHOD_POST, '/messaging/messages/push', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'messageId' => ID::unique(),
+ 'status' => 'draft',
+ 'topics' => [$push['body']['topics'][0]],
+ 'title' => 'Test-Notification',
+ 'body' => 'Test-Notification-Body',
+ ]);
+
+ $this->assertEquals(201, $push['headers']['status-code']);
+
+ $push = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/push/' . $push['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'status' => 'processing',
+ ]);
+
+ $this->assertEquals(200, $push['headers']['status-code']);
+
+ \sleep(5);
+
+ $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $push['body']['$id'], [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(1, $message['body']['deliveredTotal']);
+ $this->assertEquals(0, \count($message['body']['deliveryErrors']));
+ }
+}
diff --git a/tests/e2e/Services/Messaging/MessagingConsoleClientTest.php b/tests/e2e/Services/Messaging/MessagingConsoleClientTest.php
new file mode 100644
index 0000000000..0baa465b48
--- /dev/null
+++ b/tests/e2e/Services/Messaging/MessagingConsoleClientTest.php
@@ -0,0 +1,413 @@
+client->call(Client::METHOD_GET, '/messaging/providers/' . $providers[0]['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid/', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'providerId' => ID::unique(),
+ 'name' => 'Sengrid1',
+ 'apiKey' => 'my-apikey',
+ 'from' => 'sender-email@my-domain.com',
+ ]);
+
+ $this->assertEquals(201, $provider['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/sendgrid/' . $provider['body']['$id'], \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'sendgrid' => [
+ 'name' => 'Sendgrid2',
+ ]]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+ $this->assertCount(2, $logs['body']['logs']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['limit(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['offset(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['limit(1)', 'offset(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+
+ /**
+ * Test for FAILURE
+ */
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['limit(-1)']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['offset(-1)']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['equal("$id", "asdf")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['orderAsc("$id")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['cursorAsc("$id")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+ }
+
+ /**
+ * @depends testListTopic
+ */
+ public function testGetTopicLogs(string $topicId): void
+ {
+ /**
+ * Test for SUCCESS
+ */
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topicId . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'topicId' => ID::unique(),
+ 'name' => 'my-app',
+ 'description' => 'web app'
+ ]);
+ $this->assertEquals(201, $topic['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/messaging/topics/' . $topic['body']['$id'], \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'description' => 'updated-description'
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertCount(2, $logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['limit(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['offset(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['limit(1)', 'offset(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+
+ /**
+ * Test for FAILURE
+ */
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['limit(-1)']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['offset(-1)']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['equal("$id", "asdf")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['orderAsc("$id")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['cursorAsc("$id")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+ }
+
+ /**
+ * @depends testSendEmail
+ */
+ public function testGetMessageLogs(array $email): void
+ {
+ /**
+ * Test for SUCCESS
+ */
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $email = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'messageId' => ID::unique(),
+ 'status' => 'draft',
+ 'topics' => [ID::unique()],
+ 'subject' => 'Khali beats Undertaker',
+ 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
+ ]);
+
+ $this->assertEquals(201, $email['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'subject' => 'Khali beats John Cena!',
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+ $this->assertCount(2, $logs['body']['logs']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['limit(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['offset(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertIsNumeric($logs['body']['total']);
+
+ $logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['limit(1)', 'offset(1)'],
+ ]);
+
+ $this->assertEquals($logs['headers']['status-code'], 200);
+ $this->assertIsArray($logs['body']['logs']);
+ $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
+ $this->assertIsNumeric($logs['body']['total']);
+
+ /**
+ * Test for FAILURE
+ */
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['limit(-1)']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['offset(-1)']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['equal("$id", "asdf")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['orderAsc("$id")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => ['cursorAsc("$id")']
+ ]);
+
+ $this->assertEquals($response['headers']['status-code'], 400);
+ }
+}
diff --git a/tests/e2e/Services/Messaging/MessagingCustomClientTest.php b/tests/e2e/Services/Messaging/MessagingCustomClientTest.php
new file mode 100644
index 0000000000..ee7d0c2ad9
--- /dev/null
+++ b/tests/e2e/Services/Messaging/MessagingCustomClientTest.php
@@ -0,0 +1,14 @@
+ $provider) {
$asserted = false;
- foreach ($response['body']['providers'] as $responseProvider) {
+ foreach ($response['body']['oAuthProviders'] as $responseProvider) {
if ($responseProvider['key'] === $key) {
$this->assertEquals('AppId-' . ucfirst($key), $responseProvider['appId']);
$this->assertEquals('Secret-' . ucfirst($key), $responseProvider['secret']);
@@ -867,7 +867,7 @@ class ProjectsConsoleClientTest extends Scope
$i = 0;
foreach ($providers as $key => $provider) {
$asserted = false;
- foreach ($response['body']['providers'] as $responseProvider) {
+ foreach ($response['body']['oAuthProviders'] as $responseProvider) {
if ($responseProvider['key'] === $key) {
// On first provider, test enabled=false
$this->assertEquals($i !== 0, $responseProvider['enabled']);
diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php
index 3327bb7558..7d22f23251 100644
--- a/tests/e2e/Services/Users/UsersBase.php
+++ b/tests/e2e/Services/Users/UsersBase.php
@@ -23,6 +23,7 @@ trait UsersBase
'password' => 'password',
'name' => 'Cristiano Ronaldo',
], false);
+ $this->assertEquals($user['headers']['status-code'], 201);
// Test empty prefs is object not array
$bodyString = $user['body'];
@@ -1223,6 +1224,99 @@ trait UsersBase
$this->assertEquals($response['headers']['status-code'], 400);
}
+ /**
+ * @depends testGetUser
+ */
+ public function testCreateUserTarget(array $data): array
+ {
+ $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'providerId' => ID::unique(),
+ 'name' => 'Sengrid1',
+ 'apiKey' => 'my-apikey',
+ 'from' => 'from@domain.com',
+ ]);
+ $this->assertEquals(201, $provider['headers']['status-code']);
+ $response = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/targets', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'targetId' => ID::unique(),
+ 'providerId' => $provider['body']['$id'],
+ 'providerType' => 'email',
+ 'identifier' => 'random-email@mail.org',
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertEquals($provider['body']['$id'], $response['body']['providerId']);
+ $this->assertEquals('random-email@mail.org', $response['body']['identifier']);
+ return $response['body'];
+ }
+
+ /**
+ * @depends testCreateUserTarget
+ */
+ public function testUpdateUserTarget(array $data): array
+ {
+ $response = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'identifier' => 'random-email1@mail.org',
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('random-email1@mail.org', $response['body']['identifier']);
+ return $response['body'];
+ }
+
+ /**
+ * @depends testUpdateUserTarget
+ */
+ public function testListUserTarget(array $data)
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(2, \count($response['body']['targets']));
+ }
+
+ /**
+ * @depends testUpdateUserTarget
+ */
+ public function testGetUserTarget(array $data)
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($data['$id'], $response['body']['$id']);
+ }
+
+ /**
+ * @depends testUpdateUserTarget
+ */
+ public function testDeleteUserTarget(array $data)
+ {
+ $response = $this->client->call(Client::METHOD_DELETE, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(204, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(1, $response['body']['total']);
+ }
+
/**
* @depends testGetUser
*/
diff --git a/tests/unit/Auth/Validator/PasswordDictionaryTest.php b/tests/unit/Auth/Validator/PasswordDictionaryTest.php
index fd7f51ff16..5c8d47923c 100644
--- a/tests/unit/Auth/Validator/PasswordDictionaryTest.php
+++ b/tests/unit/Auth/Validator/PasswordDictionaryTest.php
@@ -24,5 +24,16 @@ class PasswordDictionaryTest extends TestCase
$this->assertEquals($this->object->isValid('123456'), false);
$this->assertEquals($this->object->isValid('password'), false);
$this->assertEquals($this->object->isValid('myPasswordIsRight'), true);
+
+ $pass = ''; // 256 chars
+ for ($i = 0; $i < 256; $i++) {
+ $pass .= 'p';
+ }
+
+ $this->assertEquals($this->object->isValid($pass), true);
+
+ $pass .= 'p'; // 257 chars
+
+ $this->assertEquals($this->object->isValid($pass), false);
}
}
diff --git a/tests/unit/Utopia/Response/Filters/V16Test.php b/tests/unit/Utopia/Response/Filters/V16Test.php
index fba3b69535..2078559b61 100644
--- a/tests/unit/Utopia/Response/Filters/V16Test.php
+++ b/tests/unit/Utopia/Response/Filters/V16Test.php
@@ -154,9 +154,9 @@ class V16Test extends TestCase
public function projectProvider(): array
{
return [
- 'providers' => [
+ 'oAuthProviders' => [
[
- 'providers' => [
+ 'oAuthProviders' => [
[
'key' => 'github',
'name' => 'GitHub',
@@ -167,7 +167,7 @@ class V16Test extends TestCase
],
],
[
- 'providers' => [
+ 'oAuthProviders' => [
[
'name' => 'Github',
'appId' => 'client_id',