diff --git a/.env b/.env
index feeb74d849..cb0ae9a424 100644
--- a/.env
+++ b/.env
@@ -98,4 +98,13 @@ _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_PROVIDER_MSG91_SENDER_ID=
+_APP_MESSAGE_SMS_PROVIDER_MSG91_AUTH_KEY=
+_APP_MESSAGE_SMS_PROVIDER_MSG91_FROM=
+_APP_MESSAGE_SMS_PROVIDER_MSG91_TO=
+_APP_MESSAGE_SMS_PROVIDER_MAILGUN_API_KEY=
+_APP_MESSAGE_SMS_PROVIDER_MAILGUN_DOMAIN=
+_APP_MESSAGE_SMS_PROVIDER_MAILGUN_FROM=
+_APP_MESSAGE_SMS_PROVIDER_MAILGUN_RECEIVER_EMAIL=
+_APP_MESSAGE_SMS_PROVIDER_MAILGUN_IS_EU_REGION=
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/README-CN.md b/README-CN.md
index 1823f32b1e..c4a8cc5f3c 100644
--- a/README-CN.md
+++ b/README-CN.md
@@ -2,7 +2,7 @@
-
+
适用于[Flutter/Vue/Angular/React/iOS/Android/* 等等平台 *]的完整后端服务
diff --git a/app/config/collections.php b/app/config/collections.php
index 7cc9db49cc..bf2dd07975 100644
--- a/app/config/collections.php
+++ b/app/config/collections.php
@@ -1449,6 +1449,17 @@ $commonCollections = [
'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,
@@ -1534,6 +1545,28 @@ $commonCollections = [
'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,
@@ -1567,6 +1600,17 @@ $commonCollections = [
'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,
@@ -1589,17 +1633,6 @@ $commonCollections = [
'array' => false,
'filters' => [],
],
- [
- '$id' => ID::custom('delivered'),
- 'type' => Database::VAR_BOOLEAN,
- 'format' => '',
- 'size' => 0,
- 'signed' => true,
- 'required' => false,
- 'default' => false,
- 'array' => false,
- 'filters' => [],
- ],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
@@ -1912,6 +1945,20 @@ $commonCollections = [
'attributes' => ['providerInternalId'],
'lengths' => [],
'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_identifier'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['identifier'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_identifier_providerId'),
+ 'type' => Database::INDEX_UNIQUE,
+ 'attributes' => ['providerId', 'identifier'],
+ 'lengths' => [],
+ 'orders' => [],
]
],
],
diff --git a/app/config/errors.php b/app/config/errors.php
index 575a4588ba..8001517d78 100644
--- a/app/config/errors.php
+++ b/app/config/errors.php
@@ -240,6 +240,16 @@ return [
'description' => 'OAuth2 provider returned some error.',
'code' => 424,
],
+ 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 => [
@@ -728,4 +738,28 @@ return [
'description' => 'Migration is already in progress. You can check the status of the migration in your Appwrite Console\'s "Settings" > "Migrations".',
'code' => 409,
],
+
+ /** Provider Errors */
+ 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,
+ ],
+
+ /** Message Errors */
+ Exception::MESSAGE_NOT_FOUND => [
+ 'name' => Exception::MESSAGE_NOT_FOUND,
+ 'description' => 'Message with the requested ID could not be found.',
+ 'code' => 404,
+ ]
];
diff --git a/app/config/events.php b/app/config/events.php
index 4aaa324e9c..b07d356470 100644
--- a/app/config/events.php
+++ b/app/config/events.php
@@ -258,6 +258,9 @@ return [
'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,
diff --git a/app/config/services.php b/app/config/services.php
index 3700af659a..cfefd60c8b 100644
--- a/app/config/services.php
+++ b/app/config/services.php
@@ -251,4 +251,17 @@ return [
'optional' => true,
'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/controllers/api/account.php b/app/controllers/api/account.php
index 40c4c2aca3..ffa3ba570b 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';
@@ -1238,9 +1238,12 @@ App::post('/v1/account/sessions/phone')
->inject('events')
->inject('messaging')
->inject('locale')
- ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $events, EventPhone $messaging, Locale $locale) {
-
- if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
+ ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $events, Messaging $messaging, Locale $locale) {
+ $provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['sms'])
+ ]));
+ if ($provider === false || $provider->isEmpty()) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -1326,9 +1329,34 @@ App::post('/v1/account/sessions/phone')
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
+ $target = $dbForProject->findOne('targets', [
+ Query::equal('identifier', [$phone]),
+ Query::equal('providerInternalId', [$provider->getInternalId()])
+ ]);
+
+ if (!$target) {
+ $target = $dbForProject->createDocument('targets', new Document([
+ 'userId' => $user->getId(),
+ 'userInternalId' => $user->getInternalId(),
+ 'providerId' => $provider->getId(),
+ 'providerInternalId' => $provider->getInternalId(),
+ 'identifier' => $phone,
+ ]));
+ }
+
+ $messageDoc = $dbForProject->createDocument('messages', new Document([
+ '$id' => $token->getId(),
+ 'to' => [$target->getId()],
+ 'data' => [
+ 'content' => $message,
+ ],
+ 'providerId' => $provider->getId(),
+ 'providerInternalId' => $provider->getInternalId(),
+ ]));
+
$messaging
- ->setRecipient($phone)
- ->setMessage($message)
+ ->setMessageId($messageDoc->getId())
+ ->setProject($project)
->trigger();
$events->setPayload(
@@ -2885,10 +2913,13 @@ App::post('/v1/account/verification/phone')
->inject('messaging')
->inject('project')
->inject('locale')
- ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, EventPhone $messaging, Document $project, Locale $locale) {
-
- if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
- throw new Exception(Exception::GENERAL_PHONE_DISABLED);
+ ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, Messaging $messaging, Document $project, Locale $locale) {
+ $provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['sms'])
+ ]));
+ if ($provider === false || $provider->isEmpty()) {
+ throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
if (empty($user->getAttribute('phone'))) {
@@ -2933,11 +2964,35 @@ App::post('/v1/account/verification/phone')
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
+ $target = $dbForProject->findOne('targets', [
+ Query::equal('identifier', [$user->getAttribute('phone')]),
+ Query::equal('providerInternalId', [$provider->getInternalId()])
+ ]);
+
+ if (!$target) {
+ $target = $dbForProject->createDocument('targets', new Document([
+ 'userId' => $user->getId(),
+ 'userInternalId' => $user->getInternalId(),
+ 'providerId' => $provider->getId(),
+ 'providerInternalId' => $provider->getInternalId(),
+ 'identifier' => $user->getAttribute('phone'),
+ ]));
+ }
+
+ $messageDoc = $dbForProject->createDocument('messages', new Document([
+ '$id' => $verification->getId(),
+ 'to' => [$target->getId()],
+ 'data' => [
+ 'content' => $message,
+ ],
+ 'providerId' => $provider->getId(),
+ 'providerInternalId' => $provider->getInternalId(),
+ ]));
+
$messaging
- ->setRecipient($user->getAttribute('phone'))
- ->setMessage($message)
- ->trigger()
- ;
+ ->setMessageId($messageDoc->getId())
+ ->setProject($project)
+ ->trigger();
$events
->setParam('userId', $user->getId())
diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php
new file mode 100644
index 0000000000..533086a058
--- /dev/null
+++ b/app/controllers/api/messaging.php
@@ -0,0 +1,1397 @@
+desc('Create Mailgun Provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'providers.create')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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('default', false, new Boolean(), 'Set as default provider.', true)
+ ->param('enabled', true, new Boolean(), 'Set as enabled.', true)
+ ->param('isEuRegion', false, new Boolean(), 'Set as EU region.', true)
+ ->param('from', '', new Text(256), 'Sender Email Address.')
+ ->param('apiKey', '', new Text(0), 'Mailgun API Key.')
+ ->param('domain', '', new Text(0), 'Mailgun Domain.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, bool $default, bool $enabled, bool $isEuRegion, string $from, string $apiKey, string $domain, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'mailgun',
+ 'type' => 'email',
+ 'default' => $default,
+ 'enabled' => $enabled,
+ 'search' => $providerId . ' ' . $name . ' ' . 'mailgun' . ' ' . 'email',
+ 'credentials' => [
+ 'apiKey' => $apiKey,
+ 'domain' => $domain,
+ 'isEuRegion' => $isEuRegion,
+ ],
+ 'options' => [
+ 'from' => $from,
+ ]
+ ]);
+
+ // Check if a default provider exists, if not, set this one as default
+ if (
+ empty($dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['email'])
+ ]))
+ ) {
+ $provider->setAttribute('default', true);
+ }
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $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', 'providers.create')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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('default', false, new Boolean(), 'Set as default provider.', true)
+ ->param('enabled', true, new Boolean(), 'Set as enabled.', true)
+ ->param('apiKey', '', new Text(0), 'Sendgrid API key.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, bool $default, bool $enabled, string $apiKey, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'sendgrid',
+ 'type' => 'email',
+ 'default' => $default,
+ 'enabled' => $enabled,
+ 'options' => [],
+ 'search' => $providerId . ' ' . $name . ' ' . 'sendgrid' . ' ' . 'email',
+ 'credentials' => [
+ 'apiKey' => $apiKey,
+ ],
+ ]);
+
+ // Check if a default provider exists, if not, set this one as default
+ if (
+ empty($dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['sms'])
+ ]))
+ ) {
+ $provider->setAttribute('default', true);
+ }
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $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', 'providers.create')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->label('scope', 'providers.write')
+ ->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('default', false, new Boolean(), 'Set as default provider.', true)
+ ->param('enabled', true, new Boolean(), 'Set as enabled.', true)
+ ->param('from', '', new Text(256), 'Sender Number.')
+ ->param('senderId', '', new Text(0), 'Msg91 Sender ID.')
+ ->param('authKey', '', new Text(0), 'Msg91 Auth Key.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, bool $default, bool $enabled, string $from, string $senderId, string $authKey, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'msg91',
+ 'type' => 'sms',
+ 'search' => $providerId . ' ' . $name . ' ' . 'msg91' . ' ' . 'sms',
+ 'default' => $default,
+ 'enabled' => $enabled,
+ 'credentials' => [
+ 'senderId' => $senderId,
+ 'authKey' => $authKey,
+ ],
+ 'options' => [
+ 'from' => $from,
+ ]
+ ]);
+
+ // Check if a default provider exists, if not, set this one as default
+ if (
+ empty($dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['sms'])
+ ]))
+ ) {
+ $provider->setAttribute('default', true);
+ }
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $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', 'providers.create')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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('default', false, new Boolean(), 'Set as default provider.', true)
+ ->param('enabled', true, new Boolean(), 'Set as enabled.', true)
+ ->param('username', '', new Text(0), 'Telesign username.')
+ ->param('password', '', new Text(0), 'Telesign password.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, bool $default, bool $enabled, string $username, string $password, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'telesign',
+ 'type' => 'sms',
+ 'search' => $providerId . ' ' . $name . ' ' . 'telesign' . ' ' . 'sms',
+ 'default' => $default,
+ 'enabled' => $enabled,
+ 'credentials' => [
+ 'username' => $username,
+ 'password' => $password,
+ ],
+ ]);
+
+ // Check if a default provider exists, if not, set this one as default
+ if (
+ empty($dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['sms'])
+ ]))
+ ) {
+ $provider->setAttribute('default', true);
+ }
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $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', 'providers.create')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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('default', false, new Boolean(), 'Set as default provider.', true)
+ ->param('enabled', true, new Boolean(), 'Set as enabled.', true)
+ ->param('username', '', new Text(0), 'Textmagic username.')
+ ->param('apiKey', '', new Text(0), 'Textmagic apiKey.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, bool $default, bool $enabled, string $username, string $apiKey, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'text-magic',
+ 'type' => 'sms',
+ 'search' => $providerId . ' ' . $name . ' ' . 'text-magic' . ' ' . 'sms',
+ 'default' => $default,
+ 'enabled' => $enabled,
+ 'credentials' => [
+ 'username' => $username,
+ 'apiKey' => $apiKey,
+ ],
+ ]);
+
+ // Check if a default provider exists, if not, set this one as default
+ if (
+ empty($dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['sms'])
+ ]))
+ ) {
+ $provider->setAttribute('default', true);
+ }
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $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', 'providers.create')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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('default', false, new Boolean(), 'Set as default provider.', true)
+ ->param('enabled', true, new Boolean(), 'Set as enabled.', true)
+ ->param('accountSid', '', new Text(0), 'Twilio account secret ID.')
+ ->param('authToken', '', new Text(0), 'Twilio authentication token.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, bool $default, bool $enabled, string $accountSid, string $authToken, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'twilio',
+ 'type' => 'sms',
+ 'search' => $providerId . ' ' . $name . ' ' . 'twilio' . ' ' . 'sms',
+ 'default' => $default,
+ 'enabled' => $enabled,
+ 'credentials' => [
+ 'accountSid' => $accountSid,
+ 'authToken' => $authToken,
+ ],
+ ]);
+
+ // Check if a default provider exists, if not, set this one as default
+ if (
+ empty($dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['sms'])
+ ]))
+ ) {
+ $provider->setAttribute('default', true);
+ }
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $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', 'providers.create')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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('default', false, new Boolean(), 'Set as default provider.', true)
+ ->param('enabled', true, new Boolean(), 'Set as enabled.', true)
+ ->param('apiKey', '', new Text(0), 'Vonage API key.')
+ ->param('apiSecret', '', new Text(0), 'Vonage API secret.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, bool $default, bool $enabled, string $apiKey, string $apiSecret, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'vonage',
+ 'type' => 'sms',
+ 'search' => $providerId . ' ' . $name . ' ' . 'vonage' . ' ' . 'sms',
+ 'default' => $default,
+ 'enabled' => $enabled,
+ 'credentials' => [
+ 'apiKey' => $apiKey,
+ 'apiSecret' => $apiSecret,
+ ],
+ ]);
+
+ // Check if a default provider exists, if not, set this one as default
+ if (
+ empty($dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['sms'])
+ ]))
+ ) {
+ $provider->setAttribute('default', true);
+ }
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $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', 'providers.create')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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('default', false, new Boolean(), 'Set as default provider.', true)
+ ->param('enabled', true, new Boolean(), 'Set as enabled.', true)
+ ->param('serverKey', '', new Text(0), 'FCM Server Key.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, bool $default, bool $enabled, string $serverKey, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'fcm',
+ 'type' => 'push',
+ 'search' => $providerId . ' ' . $name . ' ' . 'fcm' . ' ' . 'push',
+ 'default' => $default,
+ 'enabled' => $enabled,
+ 'credentials' => [
+ 'serverKey' => $serverKey,
+ ],
+ ]);
+
+ // Check if a default provider exists, if not, set this one as default
+ if (
+ empty($dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['push'])
+ ]))
+ ) {
+ $provider->setAttribute('default', true);
+ }
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $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', 'providers.create')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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('default', false, new Boolean(), 'Set as default provider.', true)
+ ->param('enabled', true, new Boolean(), 'Set as enabled.', true)
+ ->param('authKey', '', new Text(0), 'APNS authentication key.')
+ ->param('authKeyId', '', new Text(0), 'APNS authentication key ID.')
+ ->param('teamId', '', new Text(0), 'APNS team ID.')
+ ->param('bundleId', '', new Text(0), 'APNS bundle ID.')
+ ->param('endpoint', '', new Text(0), 'APNS endpoint.')
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, bool $default, bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, Database $dbForProject, Response $response) {
+ $providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
+ $provider = new Document([
+ '$id' => $providerId,
+ 'name' => $name,
+ 'provider' => 'apns',
+ 'type' => 'push',
+ 'search' => $providerId . ' ' . $name . ' ' . 'apns' . ' ' . 'push',
+ 'default' => $default,
+ 'enabled' => $enabled,
+ 'credentials' => [
+ 'authKey' => $authKey,
+ 'authKeyId' => $authKeyId,
+ 'teamId' => $teamId,
+ 'bundleId' => $bundleId,
+ 'endpoint' => $endpoint,
+ ],
+ ]);
+
+ // Check if a default provider exists, if not, set this one as default
+ if (
+ empty($dbForProject->findOne('providers', [
+ Query::equal('default', [true]),
+ Query::equal('type', ['push'])
+ ]))
+ ) {
+ $provider->setAttribute('default', true);
+ }
+
+ try {
+ $provider = $dbForProject->createDocument('providers', $provider);
+ } catch (DuplicateException) {
+ throw new Exception(Exception::PROVIDER_ALREADY_EXISTS);
+ }
+
+ $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)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (array $queries, Database $dbForProject, Response $response) {
+ $queries = Query::parseQueries($queries);
+
+ // 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->findOne('providers', [
+ Query::equal('$id', [$providerId]),
+ ]));
+
+ if ($cursorDocument === false || $cursorDocument->isEmpty()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $filterQueries = Query::groupByType($queries)['filters'];
+ $response->dynamic(new Document([
+ 'total' => $dbForProject->count('providers', $filterQueries, APP_LIMIT_COUNT),
+ 'providers' => $dbForProject->find('providers', $queries),
+ ]), Response::MODEL_PROVIDER_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', 'providers.update')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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 Text(256), 'Sender Email Address.', true)
+ ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true)
+ ->param('domain', '', new Text(0), 'Mailgun Domain.', true)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $from, string $apiKey, string $domain, 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);
+ $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'mailgun' . ' ' . 'email');
+ }
+
+ if (!empty($from)) {
+ $provider->setAttribute('options', [
+ 'from' => $from,
+ ]);
+ }
+
+ if ($enabled === true || $enabled === false) {
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $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);
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/sendgrid/:providerId')
+ ->desc('Update Sendgrid Provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'providers.update')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, 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);
+ $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'sendgrid' . ' ' . 'email');
+ }
+
+ if ($enabled === true || $enabled === false) {
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ if (!empty($apiKey)) {
+ $provider->setAttribute('credentials', [
+ 'apiKey' => $apiKey,
+ ]);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/msg91/:providerId')
+ ->desc('Update Msg91 Provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'providers.update')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $senderId, string $authKey, 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);
+ $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'msg91' . ' ' . 'sms');
+ }
+
+ if ($enabled === true || $enabled === false) {
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($senderId)) {
+ $credentials['senderId'] = $senderId;
+ }
+
+ if (!empty($authKey)) {
+ $credentials['authKey'] = $authKey;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/telesign/:providerId')
+ ->desc('Update Telesign Provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'providers.update')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $password, 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);
+ $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'telesign' . ' ' . 'sms');
+ }
+
+ if ($enabled === true || $enabled === false) {
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($username)) {
+ $credentials['username'] = $username;
+ }
+
+ if (!empty($password)) {
+ $credentials['password'] = $password;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/textmagic/:providerId')
+ ->desc('Update Textmagic Provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'providers.update')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $apiKey, 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 !== 'text-magic') {
+ throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
+ }
+
+ if (!empty($name)) {
+ $provider->setAttribute('name', $name);
+ $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'textmagic' . ' ' . 'sms');
+ }
+
+ if ($enabled === true || $enabled === false) {
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($username)) {
+ $credentials['username'] = $username;
+ }
+
+ if (!empty($apiKey)) {
+ $credentials['apiKey'] = $apiKey;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/twilio/:providerId')
+ ->desc('Update Twilio Provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'providers.update')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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', null, new Text(0), 'Twilio account secret ID.', true)
+ ->param('authToken', null, new Text(0), 'Twilio authentication token.', true)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $accountSid, string $authToken, 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);
+ $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'twilio' . ' ' . 'sms');
+ }
+
+ if ($enabled === true || $enabled === false) {
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($accountSid)) {
+ $credentials['accountSid'] = $accountSid;
+ }
+
+ if (!empty($authToken)) {
+ $credentials['authToken'] = $authToken;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/vonage/:providerId')
+ ->desc('Update Vonage Provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'providers.update')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $apiSecret, 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);
+ $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'vonage' . ' ' . 'sms');
+ }
+
+ if ($enabled === true || $enabled === false) {
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $credentials = $provider->getAttribute('credentials');
+
+ if (!empty($apiKey)) {
+ $credentials['apiKey'] = $apiKey;
+ }
+
+ if (!empty($apiSecret)) {
+ $credentials['apiSecret'] = $apiSecret;
+ }
+
+ $provider->setAttribute('credentials', $credentials);
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::patch('/v1/messaging/providers/fcm/:providerId')
+ ->desc('Update FCM Provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'providers.update')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $serverKey, 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);
+ $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'fcm' . ' ' . 'push');
+ }
+
+ if ($enabled === true || $enabled === false) {
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ if (!empty($serverKey)) {
+ $provider->setAttribute('credentials', ['serverKey' => $serverKey]);
+ }
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+
+App::patch('/v1/messaging/providers/apns/:providerId')
+ ->desc('Update APNS Provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'providers.update')
+ ->label('audits.resource', 'providers/{response.$id}')
+ ->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('dbForProject')
+ ->inject('response')
+ ->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, 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);
+ $provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'apns' . ' ' . 'push');
+ }
+
+ if ($enabled === true || $enabled === false) {
+ $provider->setAttribute('enabled', $enabled);
+ }
+
+ $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);
+
+ $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
+
+ $response
+ ->dynamic($provider, Response::MODEL_PROVIDER);
+ });
+
+App::delete('/v1/messaging/providers/:providerId')
+ ->desc('Delete Provider')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'providers.delete')
+ ->label('audits.resource', 'providers/{request.id}')
+ ->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('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);
+ }
+
+ $dbForProject->deleteDocument('providers', $provider->getId());
+
+ $response->noContent();
+ });
+
+App::post('/v1/messaging/messages/email')
+ ->desc('Send an email.')
+ ->groups(['api', 'messaging'])
+ ->label('audits.event', 'messages.create')
+ ->label('audits.resource', 'messages/{response.$id}')
+ ->label('scope', 'messages.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
+ ->label('sdk.namespace', 'messaging')
+ ->label('sdk.method', 'sendEmail')
+ ->label('sdk.description', '/docs/references/messaging/send-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('providerId', '', new UID(), 'Email Provider ID.')
+ ->param('to', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs or List of User IDs or List of Target IDs.')
+ ->param('subject', '', new Text(998), 'Email Subject.')
+ ->param('description', '', new Text(256), 'Description for Message.', true)
+ ->param('content', '', new Text(64230), 'Email Content.')
+ ->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status.', true)
+ ->param('html', false, new Boolean(), 'Is content of type HTML', true)
+ ->inject('dbForProject')
+ ->inject('project')
+ ->inject('messaging')
+ ->inject('response')
+ ->action(function (string $messageId, string $providerId, array $to, string $subject, string $description, string $content, string $status, bool $html, Database $dbForProject, Document $project, Messaging $messaging, Response $response) {
+ $messageId = $messageId == 'unique()' ? ID::unique() : $messageId;
+
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+
+ $message = $dbForProject->createDocument('messages', new Document([
+ '$id' => $messageId,
+ 'providerId' => $provider->getId(),
+ 'providerInternalId' => $provider->getInternalId(),
+ 'to' => $to,
+ 'data' => [
+ 'subject' => $subject,
+ 'content' => $content,
+ 'html' => $html,
+ 'description' => $description,
+ ],
+ 'status' => $status,
+ 'search' => $messageId . ' ' . $description . ' ' . $subject,
+ ]));
+
+ if ($status === 'processing') {
+ $messaging
+ ->setMessageId($message->getId())
+ ->setProject($project)
+ ->trigger();
+ }
+
+ $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 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)
+ ->inject('dbForProject')
+ ->inject('response')
+ ->action(function (array $queries, Database $dbForProject, Response $response) {
+ $queries = Query::parseQueries($queries);
+
+ // 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->findOne('messages', [
+ Query::equal('$id', [$messageId]),
+ ]));
+
+ if ($cursorDocument === false || $cursorDocument->isEmpty()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $filterQueries = Query::groupByType($queries)['filters'];
+ $response->dynamic(new Document([
+ 'total' => $dbForProject->count('messages', $filterQueries, APP_LIMIT_COUNT),
+ 'messages' => $dbForProject->find('messages', $queries),
+ ]), Response::MODEL_MESSAGE_LIST);
+ });
+
+App::get('/v1/messaging/messages/:messageId')
+ ->desc('Get 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', 'messages.update')
+ ->label('audits.resource', 'messages/{response.$id}')
+ ->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_CREATED)
+ ->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
+ ->label('sdk.response.model', Response::MODEL_MESSAGE)
+ ->param('messageId', '', new UID(), 'Message ID.')
+ ->param('to', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs or List of User IDs or List of Target 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', 'processing']), 'Message Status.', true)
+ ->param('html', false, new Boolean(), 'Is content of type HTML', true)
+ ->param('deliveryTime', DateTime::now(), new DatetimeValidator(), 'Delivery time for message.', true)
+ ->inject('dbForProject')
+ ->inject('project')
+ ->inject('messaging')
+ ->inject('response')
+ ->action(function (string $messageId, array $to, string $subject, string $description, string $content, string $status, bool $html, string $deliveryTime, Database $dbForProject, Document $project, Messaging $messaging, Response $response) {
+ $message = $dbForProject->getDocument('messages', $messageId);
+
+ if ($message->isEmpty()) {
+ throw new Exception(Exception::MESSAGE_NOT_FOUND);
+ }
+
+ if (\count($to) > 0) {
+ $message->setAttribute('to', $to);
+ }
+
+ $data = $message->getAttribute('data');
+
+ if (!empty($subject)) {
+ $data['subject'] = $subject;
+ }
+
+ if (!empty($content)) {
+ $data['content'] = $content;
+ }
+
+ if (!empty($description)) {
+ $data['description'] = $description;
+ }
+
+ if (!empty($html)) {
+ $data['html'] = $html;
+ }
+
+ $message->setAttribute('data', $data);
+ $message->setAttribute('search', $message->getId() . ' ' . $data['description'] . ' ' . $data['subject']);
+
+ if (!empty($status)) {
+ $message->setAttribute('status', $status);
+ }
+
+ $message = $dbForProject->updateDocument('messages', $message->getId(), $message);
+
+ if ($status === 'processing') {
+ $messaging
+ ->setMessageId($message->getId())
+ ->setDeliveryTime($deliveryTime)
+ ->setProject($project)
+ ->trigger();
+ }
+
+ $response
+ ->dynamic($message, Response::MODEL_MESSAGE);
+ });
diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php
index db163d5c0a..ccdf82e8ee 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;
@@ -380,6 +380,7 @@ App::post('/v1/teams/:teamId/memberships')
->param('roles', [], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.')
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project 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']) // TODO add our own built-in confirm page
->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true)
+ ->param('from', '', new Text(128), 'Sender of the message. It can be alphanumeric (Ex: MyCompany20). Restrictions may apply depending of the destination.', true)
->inject('response')
->inject('project')
->inject('user')
@@ -388,7 +389,7 @@ App::post('/v1/teams/:teamId/memberships')
->inject('mails')
->inject('messaging')
->inject('events')
- ->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 $mails, EventPhone $messaging, Event $events) {
+ ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, string $from, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, Messaging $messaging, Event $events) {
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@@ -632,6 +633,15 @@ App::post('/v1/teams/:teamId/memberships')
->trigger()
;
} elseif (!empty($phone)) {
+ $provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [
+ Query::equal('default', [true, false]),
+ Query::equal('type', ['sms'])
+ ]));
+
+ if ($provider === false || $provider->isEmpty()) {
+ 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] ?? [];
@@ -642,9 +652,29 @@ App::post('/v1/teams/:teamId/memberships')
$message = $message->setParam('{{token}}', $url);
$message = $message->render();
+ $target = $dbForProject->createDocument('targets', new Document([
+ 'userId' => $invitee->getId(),
+ 'userInternalId' => $invitee->getInternalId(),
+ 'providerId' => $provider->getId(),
+ 'providerInternalId' => $provider->getInternalId(),
+ 'identifier' => $phone,
+ ]));
+
+ $messageDoc = $dbForProject->createDocument('messages', new Document([
+ // Here membership ID is used as message ID so that it can be used in test cases to verify the message
+ '$id' => $membership->getId(),
+ 'to' => [$target->getId()],
+ 'data' => [
+ 'content' => $message,
+ ],
+ 'providerId' => $provider->getId(),
+ 'providerInternalId' => $provider->getInternalId(),
+ 'deliveryTime' => Datetime::now(),
+ ]));
+
$messaging
- ->setRecipient($phone)
- ->setMessage($message)
+ ->setMessageId($messageDoc->getId())
+ ->setProject($project)
->trigger();
}
}
diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php
index 8c97fc08b3..9551a05ed9 100644
--- a/app/controllers/api/users.php
+++ b/app/controllers/api/users.php
@@ -380,6 +380,64 @@ 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', 'users.targets.create')
+ ->label('audits.resource', 'target/response.$id')
+ ->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('userId', '', new UID(), 'User ID.')
+ ->param('targetId', '', new UID(), 'Target ID.')
+ ->param('providerId', '', new UID(), 'Provider ID.')
+ ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('events')
+ ->action(function (string $userId, string $targetId, string $providerId, string $identifier, Response $response, Database $dbForProject, Event $events) {
+ $provider = $dbForProject->getDocument('providers', $providerId);
+
+ if ($provider->isEmpty()) {
+ throw new Exception(Exception::PROVIDER_NOT_FOUND);
+ }
+
+ $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,
+ 'providerInternalId' => $provider->getInternalId(),
+ 'userId' => $userId,
+ 'userInternalId' => $user->getInternalId(),
+ 'identifier' => $identifier,
+ ]));
+ } catch (Duplicate) {
+ throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
+ }
+ $dbForProject->deleteCachedDocument('users', $user->getId());
+
+ $response
+ ->setStatusCode(Response::STATUS_CODE_CREATED)
+ ->dynamic($target, Response::MODEL_TARGET);
+ });
+
App::get('/v1/users')
->desc('List users')
->groups(['api', 'users'])
@@ -483,6 +541,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'])
@@ -647,6 +737,35 @@ 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.')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->action(function (string $userId, Response $response, Database $dbForProject) {
+
+ $user = $dbForProject->getDocument('users', $userId);
+
+ if ($user->isEmpty()) {
+ throw new Exception(Exception::USER_NOT_FOUND);
+ }
+
+ $targets = $user->getAttribute('targets', []);
+ $response->dynamic(new Document([
+ 'targets' => $targets,
+ 'total' => \count($targets),
+ ]), Response::MODEL_TARGET_LIST);
+ });
+
App::get('/v1/users/identities')
->desc('List Identities')
->groups(['api', 'users'])
@@ -1081,6 +1200,56 @@ App::patch('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
+App::patch('/v1/users/:userId/targets/:targetId/identifier')
+ ->desc('Update user target\'s identifier')
+ ->groups(['api', 'users'])
+ ->label('audits.event', 'users.targets.update')
+ ->label('audits.resource', 'target/{response.$id}')
+ ->label('scope', 'targets.write')
+ ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
+ ->label('sdk.namespace', 'users')
+ ->label('sdk.method', 'updateTargetIdentifier')
+ ->label('sdk.description', '/docs/references/users/update-target-identifier.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.)')
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('events')
+ ->action(function (string $userId, string $targetId, string $identifier, Response $response, Database $dbForProject, Event $events) {
+
+ $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);
+ }
+
+ $target->setAttribute('identifier', $identifier);
+
+ $target = $dbForProject->updateDocument('targets', $target->getId(), $target);
+ $dbForProject->deleteCachedDocument('users', $user->getId());
+
+ $events
+ ->setParam('userId', $userId)
+ ->setParam('targetId', $targetId);
+
+ $response
+ ->dynamic($target, Response::MODEL_TARGET);
+ });
+
App::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete user session')
->groups(['api', 'users'])
@@ -1211,6 +1380,48 @@ App::delete('/v1/users/:userId')
$response->noContent();
});
+App::delete('/v1/users/:userId/targets/:targetId')
+ ->desc('Delete user target')
+ ->groups(['api', 'users'])
+ ->label('audits.event', 'users.targets.delete')
+ ->label('audits.resource', 'target/{request.$targetId}')
+ ->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('response')
+ ->inject('dbForProject')
+ ->inject('events')
+ ->action(function (string $userId, string $targetId, Response $response, Database $dbForProject, Event $events) {
+
+ $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);
+ }
+
+ $target = $dbForProject->deleteDocument('targets', $target->getId());
+ $dbForProject->deleteCachedDocument('users', $user->getId());
+
+ $response->noContent();
+ });
+
App::delete('/v1/users/identities/:identityId')
->desc('Delete Identity')
->groups(['api', 'users'])
diff --git a/app/init.php b/app/init.php
index 99794a4aaf..5b40ad386c 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;
@@ -539,7 +539,6 @@ Database::addFilter(
return Authorization::skip(fn() => $database
->find('targets', [
Query::equal('userInternalId', [$document->getInternalId()]),
- Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
@@ -555,7 +554,6 @@ Database::addFilter(
$database
->find('subscribers', [
Query::equal('topicInternalId', [$document->getInternalId()]),
- Query::limit(APP_LIMIT_SUBQUERY),
])
));
if (\count($targetIds) > 0) {
@@ -926,7 +924,7 @@ App::setResource('audits', fn() => new Audit());
App::setResource('mails', fn() => new Mail());
App::setResource('deletes', fn() => new Delete());
App::setResource('database', fn() => new EventDatabase());
-App::setResource('messaging', fn() => new Phone());
+App::setResource('messaging', fn() => new Messaging());
App::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
}, ['pools']);
diff --git a/app/workers/messaging.php b/app/workers/messaging.php
index 5732c8c00b..8432e43d80 100644
--- a/app/workers/messaging.php
+++ b/app/workers/messaging.php
@@ -1,17 +1,30 @@
getUser();
- $secret = $dsn->getPassword();
-
- $this->sms = 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
- };
-
- $this->from = App::getEnv('_APP_SMS_FROM');
}
public function run(): void
{
- if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
- Console::info('Skipped sms processing. No Phone provider has been set.');
- return;
+ $project = new Document($this->args['project']);
+ $this->dbForProject = $this->getProjectDB($project);
+
+ $message = $this->dbForProject->getDocument('messages', $this->args['messageId']);
+
+ $provider = $this->dbForProject->getDocument('providers', $message->getAttribute('providerId'));
+
+ $this->processMessage($message, $provider);
+ }
+
+ private function processMessage(Document $message, Document $provider): void
+ {
+ $adapter = match ($provider->getAttribute('type')) {
+ 'sms' => $this->sms($provider),
+ 'push' => $this->push($provider),
+ 'email' => $this->email($provider),
+ default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
+ };
+
+ $recipientsId = $message->getAttribute('to');
+
+ /**
+ * @var Document[] $recipients
+ */
+ $recipients = [];
+
+ $topics = $this->dbForProject->find('topics', [Query::equal('$id', $recipientsId)]);
+ foreach ($topics as $topic) {
+ $recipients = \array_merge($recipients, $topic->getAttribute('targets'));
}
- if (empty($this->from)) {
- Console::info('Skipped sms processing. No phone number has been set.');
- return;
+ $users = $this->dbForProject->find('users', [Query::equal('$id', $recipientsId)]);
+ foreach ($users as $user) {
+ $recipients = \array_merge($recipients, $user->getAttribute('targets'));
}
- $message = new SMS(
- to: [$this->args['recipient']],
- content: $this->args['message'],
- from: $this->from,
- );
+ $targets = $this->dbForProject->find('targets', [Query::equal('$id', $recipientsId)]);
+ $recipients = \array_merge($recipients, $targets);
+ $recipients = \array_filter($recipients, function (Document $recipient) use ($provider) {
+ return $recipient->getAttribute('providerId') === $provider->getId();
+ });
- try {
- $this->sms->send($message);
- } catch (\Exception $error) {
- throw new Exception('Error sending message: ' . $error->getMessage(), 500);
+ $identifiers = \array_map(function (Document $recipient) {
+ return $recipient->getAttribute('identifier');
+ }, $recipients);
+
+ $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) {
+ $deliveredTo = 0;
+ $deliveryErrors = [];
+ $messageData = clone $message;
+ $messageData->setAttribute('to', $batch);
+ $data = match ($provider->getAttribute('type')) {
+ 'sms' => $this->buildSMSMessage($messageData, $provider),
+ 'push' => $this->buildPushMessage($messageData),
+ 'email' => $this->buildEmailMessage($messageData, $provider),
+ default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
+ };
+ try {
+ $adapter->send($data);
+ $deliveredTo += \count($batch);
+ } catch (\Exception $e) {
+ $deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage();
+ } finally {
+ $batchIndex++;
+ return [
+ 'deliveredTo' => $deliveredTo,
+ 'deliveryErrors' => $deliveryErrors,
+ ];
+ }
+ };
+ }, $batches));
+
+ $deliveredTo = 0;
+ $deliveryErrors = [];
+ foreach ($results as $result) {
+ $deliveredTo += $result['deliveredTo'];
+ $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->setAttribute('to', $recipientsId);
+ $message->setAttribute('deliveredTo', $deliveredTo);
+ $message->setAttribute('deliveredAt', DateTime::now());
+
+ $this->dbForProject->updateDocument('messages', $message->getId(), $message);
}
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']),
+ 'text-magic' => 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/composer.json b/composer.json
index 0d415677ce..ed5cfbd825 100644
--- a/composer.json
+++ b/composer.json
@@ -56,7 +56,7 @@
"utopia-php/image": "0.5.*",
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.3.*",
- "utopia-php/messaging": "0.1.*",
+ "utopia-php/messaging": "0.2.*",
"utopia-php/migration": "0.3.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.4.*",
diff --git a/composer.lock b/composer.lock
index 8c9db35f78..8abede5fa4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "13a3bdc7c1dec5756bf58ec73a49753d",
+ "content-hash": "34cb0b1c81424d1858df197aed030793",
"packages": [
{
"name": "adhocore/jwt",
@@ -1050,16 +1050,16 @@
},
{
"name": "matomo/device-detector",
- "version": "6.1.5",
+ "version": "6.1.6",
"source": {
"type": "git",
"url": "https://github.com/matomo-org/device-detector.git",
- "reference": "40ca2990dba2c1719e5c62168e822e0b86c167d4"
+ "reference": "5cbea85106e561c7138d03603eb6e05128480409"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/40ca2990dba2c1719e5c62168e822e0b86c167d4",
- "reference": "40ca2990dba2c1719e5c62168e822e0b86c167d4",
+ "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/5cbea85106e561c7138d03603eb6e05128480409",
+ "reference": "5cbea85106e561c7138d03603eb6e05128480409",
"shasum": ""
},
"require": {
@@ -1115,7 +1115,7 @@
"source": "https://github.com/matomo-org/matomo",
"wiki": "https://dev.matomo.org/"
},
- "time": "2023-08-17T16:17:41+00:00"
+ "time": "2023-10-02T10:01:54+00:00"
},
{
"name": "mongodb/mongodb",
@@ -2152,16 +2152,16 @@
},
{
"name": "utopia-php/database",
- "version": "0.43.4",
+ "version": "0.43.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
- "reference": "cabdd02e8dc1732eb0b22007c511e7bb3caa5c8c"
+ "reference": "5f7b05189cfbcc0506090498c580c5765375a00a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/database/zipball/cabdd02e8dc1732eb0b22007c511e7bb3caa5c8c",
- "reference": "cabdd02e8dc1732eb0b22007c511e7bb3caa5c8c",
+ "url": "https://api.github.com/repos/utopia-php/database/zipball/5f7b05189cfbcc0506090498c580c5765375a00a",
+ "reference": "5f7b05189cfbcc0506090498c580c5765375a00a",
"shasum": ""
},
"require": {
@@ -2202,9 +2202,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
- "source": "https://github.com/utopia-php/database/tree/0.43.4"
+ "source": "https://github.com/utopia-php/database/tree/0.43.5"
},
- "time": "2023-09-28T09:00:05+00:00"
+ "time": "2023-10-06T06:49:47+00:00"
},
{
"name": "utopia-php/domains",
@@ -2516,16 +2516,16 @@
},
{
"name": "utopia-php/messaging",
- "version": "0.1.1",
+ "version": "0.2.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/messaging.git",
- "reference": "a75d66ddd59b834ab500a4878a2c084e6572604a"
+ "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/messaging/zipball/a75d66ddd59b834ab500a4878a2c084e6572604a",
- "reference": "a75d66ddd59b834ab500a4878a2c084e6572604a",
+ "url": "https://api.github.com/repos/utopia-php/messaging/zipball/2d0f474a106bb1da285f85e105c29b46085d3a43",
+ "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43",
"shasum": ""
},
"require": {
@@ -2534,8 +2534,8 @@
},
"require-dev": {
"laravel/pint": "^1.2",
- "phpmailer/phpmailer": "6.6.*",
- "phpunit/phpunit": "9.5.*"
+ "phpmailer/phpmailer": "6.8.*",
+ "phpunit/phpunit": "9.6.*"
},
"type": "library",
"autoload": {
@@ -2558,9 +2558,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/messaging/issues",
- "source": "https://github.com/utopia-php/messaging/tree/0.1.1"
+ "source": "https://github.com/utopia-php/messaging/tree/0.2.0"
},
- "time": "2023-02-07T05:42:46+00:00"
+ "time": "2023-09-14T20:48:42+00:00"
},
{
"name": "utopia-php/migration",
@@ -6019,5 +6019,5 @@
"platform-overrides": {
"php": "8.0"
},
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/docker-compose.yml b/docker-compose.yml
index b229609d5b..f0ddbfe41b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -188,6 +188,15 @@ services:
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
- _APP_ASSISTANT_OPENAI_API_KEY
+ - _APP_MESSAGE_SMS_PROVIDER_MSG91_SENDER_ID
+ - _APP_MESSAGE_SMS_PROVIDER_MSG91_AUTH_KEY
+ - _APP_MESSAGE_SMS_PROVIDER_MSG91_FROM
+ - _APP_MESSAGE_SMS_PROVIDER_MSG91_TO
+ - _APP_MESSAGE_SMS_PROVIDER_MAILGUN_API_KEY
+ - _APP_MESSAGE_SMS_PROVIDER_MAILGUN_DOMAIN
+ - _APP_MESSAGE_SMS_PROVIDER_MAILGUN_FROM
+ - _APP_MESSAGE_SMS_PROVIDER_MAILGUN_RECEIVER_EMAIL
+ - _APP_MESSAGE_SMS_PROVIDER_MAILGUN_IS_EU_REGION
appwrite-realtime:
entrypoint: realtime
@@ -571,8 +580,11 @@ services:
- _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
diff --git a/phpunit.xml b/phpunit.xml
index f83f9f0fae..cffe166336 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -32,6 +32,7 @@