diff --git a/.env b/.env index 6ea21eed9d..f77083a035 100644 --- a/.env +++ b/.env @@ -87,7 +87,7 @@ _APP_LOGGING_PROVIDER= _APP_LOGGING_CONFIG= _APP_GRAPHQL_MAX_BATCH_SIZE=10 _APP_GRAPHQL_MAX_COMPLEXITY=250 -_APP_GRAPHQL_MAX_DEPTH=3 +_APP_GRAPHQL_MAX_DEPTH=4 _APP_DOCKER_HUB_USERNAME= _APP_DOCKER_HUB_PASSWORD= _APP_VCS_GITHUB_APP_NAME= diff --git a/CHANGES.md b/CHANGES.md index 889f65e1e7..fbe1e548db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -466,7 +466,7 @@ ## Features - Added Phone Authentication by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3357 - Added Twilio Support - - Added TextMagic Support + - Added Textmagic Support - Added Telesign Support - Added Endpoint to create Phone Session (`POST:/v1/account/sessions/phone`) - Added Endpoint to confirm Phone Session (`PUT:/v1/account/sessions/phone`) diff --git a/app/config/collections.php b/app/config/collections.php index a11bd03eb1..fbf7b71983 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1505,6 +1505,17 @@ $commonCollections = [ '$id' => ID::custom('messages'), 'name' => 'Messages', 'attributes' => [ + [ + '$id' => ID::custom('providerType'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('description'), 'type' => Database::VAR_STRING, @@ -1572,7 +1583,7 @@ $commonCollections = [ 'filters' => [], ], [ - '$id' => ID::custom('deliveryTime'), + '$id' => ID::custom('scheduledAt'), 'type' => Database::VAR_DATETIME, 'format' => '', 'size' => 0, @@ -1722,28 +1733,6 @@ $commonCollections = [ '$id' => ID::custom('subscribers'), 'name' => 'Subscribers', 'attributes' => [ - [ - '$id' => ID::custom('userId'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => ID::custom('userInternalId'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], - ], [ '$id' => ID::custom('targetId'), 'type' => Database::VAR_STRING, @@ -1766,6 +1755,28 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('userId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('userInternalId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => ID::custom('topicId'), 'type' => Database::VAR_STRING, @@ -1788,22 +1799,19 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('providerType'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ - [ - '$id' => ID::custom('_key_userId'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['userId'], - 'lengths' => [], - 'orders' => [], - ], - [ - '$id' => ID::custom('_key_userInternalId'), - 'type' => Database::INDEX_KEY, - 'attributes' => ['userInternalId'], - 'lengths' => [], - 'orders' => [], - ], [ '$id' => ID::custom('_key_targetId'), 'type' => Database::INDEX_KEY, @@ -1818,6 +1826,20 @@ $commonCollections = [ 'lengths' => [], 'orders' => [], ], + [ + '$id' => ID::custom('_key_userId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['userId'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_userInternalId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['userInternalId'], + 'lengths' => [], + 'orders' => [], + ], [ '$id' => ID::custom('_key_topicId'), 'type' => Database::INDEX_KEY, diff --git a/app/config/errors.php b/app/config/errors.php index f7c9e8e55f..4c811dd53e 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -103,6 +103,16 @@ return [ 'description' => 'This method was not fully implemented yet. If you believe this is a mistake, please upgrade your Appwrite server version.', 'code' => 405, ], + Exception::GENERAL_INVALID_EMAIL => [ + 'name' => Exception::GENERAL_INVALID_EMAIL, + 'description' => 'Value must be a valid email address.', + 'code' => 400, + ], + Exception::GENERAL_INVALID_PHONE => [ + 'name' => Exception::GENERAL_INVALID_PHONE, + 'description' => 'Value must be a valid phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', + 'code' => 400, + ], /** User Errors */ Exception::USER_COUNT_EXCEEDED => [ @@ -837,5 +847,20 @@ return [ 'description' => 'Message with the requested ID has already been scheduled for delivery.', 'code' => 400, ], + Exception::MESSAGE_TARGET_NOT_EMAIL => [ + 'name' => Exception::MESSAGE_TARGET_NOT_EMAIL, + 'description' => 'Message with the target ID is not an email target:', + 'code' => 400, + ], + Exception::MESSAGE_TARGET_NOT_SMS => [ + 'name' => Exception::MESSAGE_TARGET_NOT_SMS, + 'description' => 'Message with the target ID is not an SMS target:', + 'code' => 400, + ], + Exception::MESSAGE_TARGET_NOT_PUSH => [ + 'name' => Exception::MESSAGE_TARGET_NOT_PUSH, + 'description' => 'Message with the target ID is not a push target:', + 'code' => 400, + ], ]; diff --git a/app/config/variables.php b/app/config/variables.php index 6f49d4a5da..c9329f6d55 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -440,7 +440,7 @@ return [ 'variables' => [ [ 'name' => '_APP_SMS_PROVIDER', - 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, textmagic, telesign, msg91, and vonage.", + 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, Textmagic, telesign, msg91, and vonage.", 'introduction' => '0.15.0', 'default' => '', 'required' => false, diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index eda29656fe..68e3261a8d 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -149,18 +149,20 @@ App::post('/v1/account') ]); $user->removeAttribute('$internalId'); $user = Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); - $target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([ - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::user($userId)), - Permission::delete(Role::user($userId)), - ], - 'userId' => $user->getId(), - 'userInternalId' => $user->getInternalId(), - 'providerType' => 'email', - 'identifier' => $email, - ]))); - $user->setAttribute('targets', [$target]); + try { + $target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([ + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => MESSAGE_TYPE_EMAIL, + 'identifier' => $email, + ]))); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]); + } catch (Duplicate) { + $existingTarget = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ]); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]); + } $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate) { throw new Exception(Exception::USER_ALREADY_EXISTS); @@ -678,7 +680,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ], 'userId' => $userDoc->getId(), 'userInternalId' => $userDoc->getInternalId(), - 'providerType' => 'email', + 'providerType' => MESSAGE_TYPE_EMAIL, 'identifier' => $email, ])); } catch (Duplicate) { @@ -1309,6 +1311,21 @@ App::post('/v1/account/sessions/phone') $user->removeAttribute('$internalId'); Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + try { + $target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([ + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => MESSAGE_TYPE_SMS, + 'identifier' => $phone, + ]))); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]); + } catch (Duplicate) { + $existingTarget = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$phone]), + ]); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } $secret = Auth::codeGenerator(); @@ -1357,7 +1374,7 @@ App::post('/v1/account/sessions/phone') $queueForMessaging ->setMessage($messageDoc) ->setRecipients([$phone]) - ->setProviderType('SMS') + ->setProviderType(MESSAGE_TYPE_SMS) ->setProject($project) ->trigger(); @@ -1732,7 +1749,7 @@ App::post('/v1/account/targets/push') ], 'providerId' => $providerId ?? null, 'providerInternalId' => $provider->getInternalId() ?? null, - 'providerType' => 'push', + 'providerType' => MESSAGE_TYPE_PUSH, 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), 'identifier' => $identifier, @@ -2102,25 +2119,25 @@ App::patch('/v1/account/email') ->setAttribute('passwordUpdate', DateTime::now()); } - $target = $dbForProject->findOne('targets', [ + $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ Query::equal('identifier', [$email]), - ]); + ])); - if ($target && !$target->isEmpty()) { + if ($target instanceof Document && !$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } - /** - * @var Document $oldTarget - */ - $oldTarget = $user->find('identifier', $oldEmail, 'targets'); - - if ($oldTarget !== false && !$oldTarget->isEmpty()) { - $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)); - } - try { $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldEmail, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email))); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -2165,18 +2182,15 @@ App::patch('/v1/account/phone') throw new Exception(Exception::USER_INVALID_CREDENTIALS); } - $target = $dbForProject->findOne('targets', [ + $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ Query::equal('identifier', [$phone]), - ]); + ])); - if ($target && !$target->isEmpty()) { + if ($target instanceof Document && !$target->isEmpty()) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } - /** - * @var Document $oldTarget - */ - $oldTarget = $user->find('identifier', $user->getAttribute('phone'), 'targets'); + $oldPhone = $user->getAttribute('phone'); $user ->setAttribute('phone', $phone) @@ -2191,12 +2205,17 @@ App::patch('/v1/account/phone') ->setAttribute('passwordUpdate', DateTime::now()); } - if ($oldTarget !== false && !$oldTarget->isEmpty()) { - $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone)); - } - try { $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldPhone, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone))); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate $th) { throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS); } @@ -3081,7 +3100,7 @@ App::post('/v1/account/verification/phone') $queueForMessaging ->setMessage($messageDoc) ->setRecipients([$user->getAttribute('phone')]) - ->setProviderType('SMS') + ->setProviderType(MESSAGE_TYPE_SMS) ->setProject($project) ->trigger(); diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 35ba97c1af..f0cb4dd966 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -54,31 +54,57 @@ App::post('/v1/messaging/providers/mailgun') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Email(), 'Sender email address.') - ->param('apiKey', '', new Text(0), 'Mailgun API Key.') - ->param('domain', '', new Text(0), 'Mailgun Domain.') - ->param('isEuRegion', false, new Boolean(), 'Set as EU region.') - ->param('enabled', true, new Boolean(), 'Set as enabled.', true) + ->param('from', '', new Email(), 'Sender email address.', true) + ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) + ->param('domain', '', new Text(0), 'Mailgun Domain.', true) + ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $apiKey, string $domain, bool $isEuRegion, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $from, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if ($isEuRegion === true || $isEuRegion === false) { + $credentials['isEuRegion'] = $isEuRegion; + } + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + if (!empty($domain)) { + $credentials['domain'] = $domain; + } + + if ( + $enabled === true && + \array_key_exists('isEuRegion', $credentials) && + \array_key_exists('apiKey', $credentials) && + \array_key_exists('domain', $credentials) && + \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'mailgun', - 'type' => 'email', + 'type' => MESSAGE_TYPE_EMAIL, 'enabled' => $enabled, - 'credentials' => [ - 'apiKey' => $apiKey, - 'domain' => $domain, - 'isEuRegion' => $isEuRegion, - ], - 'options' => [ - 'from' => $from, - ] + 'credentials' => $credentials, + 'options' => $options, ]); try { @@ -111,26 +137,45 @@ App::post('/v1/messaging/providers/sendgrid') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Email(), 'Sender email address.') - ->param('apiKey', '', new Text(0), 'Sendgrid API key.') - ->param('enabled', true, new Boolean(), 'Set as enabled.', true) + ->param('from', '', new Email(), 'Sender email address.', true) + ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $apiKey, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $from, string $apiKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + if ( + $enabled === true + && \array_key_exists('apiKey', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'sendgrid', - 'type' => 'email', + 'type' => MESSAGE_TYPE_EMAIL, 'enabled' => $enabled, - 'credentials' => [ - 'apiKey' => $apiKey, - ], - 'options' => [ - 'from' => $from, - ] + 'credentials' => $credentials, + 'options' => $options, ]); try { @@ -163,28 +208,51 @@ App::post('/v1/messaging/providers/msg91') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') - ->param('senderId', '', new Text(0), 'Msg91 Sender ID.') - ->param('authKey', '', new Text(0), 'Msg91 Auth Key.') - ->param('enabled', true, new Boolean(), 'Set as enabled.', true) + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('senderId', '', new Text(0), 'Msg91 Sender ID.', true) + ->param('authKey', '', new Text(0), 'Msg91 Auth Key.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $senderId, string $authKey, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $from, string $senderId, string $authKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($senderId)) { + $credentials['senderId'] = $senderId; + } + + if (!empty($authKey)) { + $credentials['authKey'] = $authKey; + } + + if ( + $enabled === true + && \array_key_exists('senderId', $credentials) + && \array_key_exists('authKey', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'msg91', - 'type' => 'sms', + 'type' => MESSAGE_TYPE_SMS, 'enabled' => $enabled, - 'credentials' => [ - 'senderId' => $senderId, - 'authKey' => $authKey, - ], - 'options' => [ - 'from' => $from, - ] + 'credentials' => $credentials, + 'options' => $options, ]); try { @@ -217,28 +285,51 @@ App::post('/v1/messaging/providers/telesign') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') - ->param('username', '', new Text(0), 'Telesign username.') - ->param('password', '', new Text(0), 'Telesign password.') - ->param('enabled', true, new Boolean(), 'Set as enabled.', true) + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('username', '', new Text(0), 'Telesign username.', true) + ->param('password', '', new Text(0), 'Telesign password.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $username, string $password, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $from, string $username, string $password, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($username)) { + $credentials['username'] = $username; + } + + if (!empty($password)) { + $credentials['password'] = $password; + } + + if ( + $enabled === true + && \array_key_exists('username', $credentials) + && \array_key_exists('password', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'telesign', - 'type' => 'sms', + 'type' => MESSAGE_TYPE_SMS, 'enabled' => $enabled, - 'credentials' => [ - 'username' => $username, - 'password' => $password, - ], - 'options' => [ - 'from' => $from, - ] + 'credentials' => $credentials, + 'options' => $options, ]); try { @@ -256,7 +347,7 @@ App::post('/v1/messaging/providers/telesign') }); App::post('/v1/messaging/providers/textmagic') - ->desc('Create TextMagic provider') + ->desc('Create Textmagic provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.create') ->label('audits.resource', 'provider/{response.$id}') @@ -264,35 +355,58 @@ App::post('/v1/messaging/providers/textmagic') ->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.method', 'createTextmagicProvider') ->label('sdk.description', '/docs/references/messaging/create-textmagic-provider.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') - ->param('username', '', new Text(0), 'Textmagic username.') - ->param('apiKey', '', new Text(0), 'Textmagic apiKey.') - ->param('enabled', true, new Boolean(), 'Set as enabled.', true) + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('username', '', new Text(0), 'Textmagic username.', true) + ->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $username, string $apiKey, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $from, string $username, string $apiKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($username)) { + $credentials['username'] = $username; + } + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + if ( + $enabled === true + && \array_key_exists('username', $credentials) + && \array_key_exists('apiKey', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'textmagic', - 'type' => 'sms', + 'type' => MESSAGE_TYPE_SMS, 'enabled' => $enabled, - 'credentials' => [ - 'username' => $username, - 'apiKey' => $apiKey, - ], - 'options' => [ - 'from' => $from, - ] + 'credentials' => $credentials, + 'options' => $options, ]); try { @@ -325,28 +439,51 @@ App::post('/v1/messaging/providers/twilio') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') - ->param('accountSid', '', new Text(0), 'Twilio account secret ID.') - ->param('authToken', '', new Text(0), 'Twilio authentication token.') - ->param('enabled', true, new Boolean(), 'Set as enabled.', true) + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true) + ->param('authToken', '', new Text(0), 'Twilio authentication token.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $accountSid, string $authToken, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $from, string $accountSid, string $authToken, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($accountSid)) { + $credentials['accountSid'] = $accountSid; + } + + if (!empty($authToken)) { + $credentials['authToken'] = $authToken; + } + + if ( + $enabled === true + && \array_key_exists('accountSid', $credentials) + && \array_key_exists('authToken', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'twilio', - 'type' => 'sms', + 'type' => MESSAGE_TYPE_SMS, 'enabled' => $enabled, - 'credentials' => [ - 'accountSid' => $accountSid, - 'authToken' => $authToken, - ], - 'options' => [ - 'from' => $from, - ] + 'credentials' => $credentials, + 'options' => $options, ]); try { @@ -379,28 +516,51 @@ App::post('/v1/messaging/providers/vonage') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.') - ->param('apiKey', '', new Text(0), 'Vonage API key.') - ->param('apiSecret', '', new Text(0), 'Vonage API secret.') - ->param('enabled', true, new Boolean(), 'Set as enabled.', true) + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('apiKey', '', new Text(0), 'Vonage API key.', true) + ->param('apiSecret', '', new Text(0), 'Vonage API secret.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $apiKey, string $apiSecret, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $from, string $apiKey, string $apiSecret, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + if (!empty($apiSecret)) { + $credentials['apiSecret'] = $apiSecret; + } + + if ( + $enabled === true + && \array_key_exists('apiKey', $credentials) + && \array_key_exists('apiSecret', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'vonage', - 'type' => 'sms', + 'type' => MESSAGE_TYPE_SMS, 'enabled' => $enabled, - 'credentials' => [ - 'apiKey' => $apiKey, - 'apiSecret' => $apiSecret, - ], - 'options' => [ - 'from' => $from, - ] + 'credentials' => $credentials, + 'options' => $options, ]); try { @@ -433,22 +593,33 @@ App::post('/v1/messaging/providers/fcm') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('serverKey', '', new Text(0), 'FCM server key.') - ->param('enabled', true, new Boolean(), 'Set as enabled.', true) + ->param('serverKey', '', new Text(0), 'FCM server key.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $serverKey, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $serverKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $credentials = []; + + if (!empty($serverKey)) { + $credentials['serverKey'] = $serverKey; + } + + if ($enabled === true && \array_key_exists('serverKey', $credentials)) { + $enabled = true; + } else { + $enabled = false; + } + $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'fcm', - 'type' => 'push', + 'type' => MESSAGE_TYPE_PUSH, 'enabled' => $enabled, - 'credentials' => [ - 'serverKey' => $serverKey, - ], + 'credentials' => $credentials ]); try { @@ -481,30 +652,60 @@ App::post('/v1/messaging/providers/apns') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('authKey', '', new Text(0), 'APNS authentication key.') - ->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.') - ->param('enabled', true, 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) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $credentials = []; + + if (!empty($authKey)) { + $credentials['authKey'] = $authKey; + } + + if (!empty($authKeyId)) { + $credentials['authKeyId'] = $authKeyId; + } + + if (!empty($teamId)) { + $credentials['teamId'] = $teamId; + } + + if (!empty($bundleId)) { + $credentials['bundleId'] = $bundleId; + } + + if (!empty($endpoint)) { + $credentials['endpoint'] = $endpoint; + } + + if ( + $enabled === true + && \array_key_exists('authKey', $credentials) + && \array_key_exists('authKeyId', $credentials) + && \array_key_exists('teamId', $credentials) + && \array_key_exists('bundleId', $credentials) + && \array_key_exists('endpoint', $credentials) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider = new Document([ '$id' => $providerId, 'name' => $name, 'provider' => 'apns', - 'type' => 'push', + 'type' => MESSAGE_TYPE_PUSH, 'enabled' => $enabled, - 'credentials' => [ - 'authKey' => $authKey, - 'authKeyId' => $authKeyId, - 'teamId' => $teamId, - 'bundleId' => $bundleId, - 'endpoint' => $endpoint, - ], + 'credentials' => $credentials, ]); try { @@ -689,7 +890,7 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') ->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('from', '', new Email(), 'Sender email address.', true) ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->inject('queueForEvents') @@ -717,10 +918,6 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') ]); } - if ($enabled === true || $enabled === false) { - $provider->setAttribute('enabled', $enabled); - } - $credentials = $provider->getAttribute('credentials'); if ($isEuRegion === true || $isEuRegion === false) { @@ -737,6 +934,21 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') $provider->setAttribute('credentials', $credentials); + if ($enabled === true || $enabled === false) { + if ( + $enabled === true && + \array_key_exists('isEuRegion', $credentials) && + \array_key_exists('apiKey', $credentials) && + \array_key_exists('domain', $credentials) && + \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider->setAttribute('enabled', $enabled); + } + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); $queueForEvents @@ -764,7 +976,7 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) - ->param('from', '', new Text(256), 'Sender email address.', true) + ->param('from', '', new Email(), 'Sender email address.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') @@ -790,16 +1002,25 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') ]); } - if ($enabled === true || $enabled === false) { - $provider->setAttribute('enabled', $enabled); - } - if (!empty($apiKey)) { $provider->setAttribute('credentials', [ 'apiKey' => $apiKey, ]); } + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('apiKey', $provider->getAttribute('credentials')) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider->setAttribute('enabled', $enabled); + } + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); $queueForEvents @@ -854,10 +1075,6 @@ App::patch('/v1/messaging/providers/msg91/:providerId') ]); } - if ($enabled === true || $enabled === false) { - $provider->setAttribute('enabled', $enabled); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($senderId)) { @@ -870,6 +1087,20 @@ App::patch('/v1/messaging/providers/msg91/:providerId') $provider->setAttribute('credentials', $credentials); + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('senderId', $credentials) + && \array_key_exists('authKey', $credentials) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider->setAttribute('enabled', $enabled); + } + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); $queueForEvents @@ -924,10 +1155,6 @@ App::patch('/v1/messaging/providers/telesign/:providerId') ]); } - if ($enabled === true || $enabled === false) { - $provider->setAttribute('enabled', $enabled); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($username)) { @@ -940,6 +1167,21 @@ App::patch('/v1/messaging/providers/telesign/:providerId') $provider->setAttribute('credentials', $credentials); + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('username', $credentials) + && \array_key_exists('password', $credentials) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); $queueForEvents @@ -950,7 +1192,7 @@ App::patch('/v1/messaging/providers/telesign/:providerId') }); App::patch('/v1/messaging/providers/textmagic/:providerId') - ->desc('Update TextMagic provider') + ->desc('Update Textmagic provider') ->groups(['api', 'messaging']) ->label('audits.event', 'provider.update') ->label('audits.resource', 'provider/{response.$id}') @@ -958,7 +1200,7 @@ App::patch('/v1/messaging/providers/textmagic/:providerId') ->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.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) @@ -994,10 +1236,6 @@ App::patch('/v1/messaging/providers/textmagic/:providerId') ]); } - if ($enabled === true || $enabled === false) { - $provider->setAttribute('enabled', $enabled); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($username)) { @@ -1010,6 +1248,21 @@ App::patch('/v1/messaging/providers/textmagic/:providerId') $provider->setAttribute('credentials', $credentials); + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('username', $credentials) + && \array_key_exists('apiKey', $credentials) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); $queueForEvents @@ -1036,8 +1289,8 @@ App::patch('/v1/messaging/providers/twilio/:providerId') ->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) + ->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true) + ->param('authToken', '', new Text(0), 'Twilio authentication token.', true) ->param('from', '', new Text(256), 'Sender number.', true) ->inject('queueForEvents') ->inject('dbForProject') @@ -1064,10 +1317,6 @@ App::patch('/v1/messaging/providers/twilio/:providerId') ]); } - if ($enabled === true || $enabled === false) { - $provider->setAttribute('enabled', $enabled); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($accountSid)) { @@ -1080,6 +1329,21 @@ App::patch('/v1/messaging/providers/twilio/:providerId') $provider->setAttribute('credentials', $credentials); + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('accountSid', $credentials) + && \array_key_exists('authToken', $credentials) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); $queueForEvents @@ -1134,10 +1398,6 @@ App::patch('/v1/messaging/providers/vonage/:providerId') ]); } - if ($enabled === true || $enabled === false) { - $provider->setAttribute('enabled', $enabled); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($apiKey)) { @@ -1150,6 +1410,21 @@ App::patch('/v1/messaging/providers/vonage/:providerId') $provider->setAttribute('credentials', $credentials); + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('apiKey', $credentials) + && \array_key_exists('apiSecret', $credentials) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); $queueForEvents @@ -1196,14 +1471,20 @@ App::patch('/v1/messaging/providers/fcm/:providerId') $provider->setAttribute('name', $name); } - if ($enabled === true || $enabled === false) { - $provider->setAttribute('enabled', $enabled); - } - if (!empty($serverKey)) { $provider->setAttribute('credentials', ['serverKey' => $serverKey]); } + if ($enabled === true || $enabled === false) { + if ($enabled === true && \array_key_exists('serverKey', $provider->getAttribute('credentials'))) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); $queueForEvents @@ -1255,10 +1536,6 @@ App::patch('/v1/messaging/providers/apns/:providerId') $provider->setAttribute('name', $name); } - if ($enabled === true || $enabled === false) { - $provider->setAttribute('enabled', $enabled); - } - $credentials = $provider->getAttribute('credentials'); if (!empty($authKey)) { @@ -1283,6 +1560,23 @@ App::patch('/v1/messaging/providers/apns/:providerId') $provider->setAttribute('credentials', $credentials); + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('authKey', $credentials) + && \array_key_exists('authKeyId', $credentials) + && \array_key_exists('teamId', $credentials) + && \array_key_exists('bundleId', $credentials) + && \array_key_exists('endpoint', $credentials) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); $queueForEvents @@ -1652,14 +1946,15 @@ App::post('/v1/messaging/topics/:topicId/subscribers') '$id' => $subscriberId, '$permissions' => [ Permission::read(Role::user($user->getId())), - Permission::delete(Role::user($target->getAttribute('userId'))), + Permission::delete(Role::user($user->getId())), ], - 'userId' => $user->getId(), - 'userInternalId' => $user->getInternalId(), 'topicId' => $topicId, 'topicInternalId' => $topic->getInternalId(), 'targetId' => $targetId, 'targetInternalId' => $target->getInternalId(), + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => $target->getAttribute('providerType'), ]); try { @@ -1673,7 +1968,9 @@ App::post('/v1/messaging/topics/:topicId/subscribers') ->setParam('topicId', $topic->getId()) ->setParam('subscriberId', $subscriber->getId()); - $subscriber->setAttribute('userName', $user->getAttribute('name')); + $subscriber + ->setAttribute('target', $target) + ->setAttribute('userName', $user->getAttribute('name')); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -1693,11 +1990,16 @@ App::get('/v1/messaging/topics/:topicId/subscribers') ->label('sdk.response.model', Response::MODEL_SUBSCRIBER_LIST) ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.') ->param('queries', [], new Subscribers(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Providers::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $topicId, array $queries, Database $dbForProject, Response $response) { + ->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response) { $queries = Query::parseQueries($queries); + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); if ($topic->isEmpty()) { @@ -1725,9 +2027,11 @@ App::get('/v1/messaging/topics/:topicId/subscribers') $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) { return function () use ($subscriber, $dbForProject) { - $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $subscriber->getAttribute('userId'))); + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); return $subscriber + ->setAttribute('target', $target) ->setAttribute('userName', $user->getAttribute('name')); }; }, $subscribers)); @@ -1851,9 +2155,12 @@ App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') throw new Exception(Exception::SUBSCRIBER_NOT_FOUND); } - $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $subscriber->getAttribute('userId'))); + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); - $subscriber->setAttribute('userName', $user->getAttribute('name')); + $subscriber + ->setAttribute('target', $target) + ->setAttribute('userName', $user->getAttribute('name')); $response ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); @@ -1912,7 +2219,7 @@ App::post('/v1/messaging/messages/email') ->label('scope', 'messages.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'messaging') - ->label('sdk.method', 'createEmail') + ->label('sdk.method', 'createEmailMessage') ->label('sdk.description', '/docs/references/messaging/create-email.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) @@ -1924,23 +2231,36 @@ App::post('/v1/messaging/messages/email') ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true) ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) ->param('description', '', new Text(256), 'Description for message.', true) - ->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) + ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) + ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, string $description, string $status, bool $html, ?string $deliveryTime, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, string $description, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { throw new Exception(Exception::MESSAGE_MISSING_TARGET); } + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_EMAIL) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL . ' ' . $targetDocument->getId()); + } + } + $message = $dbForProject->createDocument('messages', new Document([ '$id' => $messageId, + 'providerType' => MESSAGE_TYPE_EMAIL, 'topics' => $topics, 'users' => $users, 'targets' => $targets, @@ -1977,7 +2297,7 @@ App::post('/v1/messaging/messages/sms') ->label('scope', 'messages.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'messaging') - ->label('sdk.method', 'createSMS') + ->label('sdk.method', 'createSMSMessage') ->label('sdk.description', '/docs/references/messaging/create-sms.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) @@ -1988,22 +2308,35 @@ App::post('/v1/messaging/messages/sms') ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true) ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) ->param('description', '', new Text(256), 'Description for Message.', true) - ->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) + ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $content, array $topics, array $users, array $targets, string $description, string $status, ?string $deliveryTime, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $content, array $topics, array $users, array $targets, string $description, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { throw new Exception(Exception::MESSAGE_MISSING_TARGET); } + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_SMS) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS . ' ' . $targetDocument->getId()); + } + } + $message = $dbForProject->createDocument('messages', new Document([ '$id' => $messageId, + 'providerType' => MESSAGE_TYPE_SMS, 'topics' => $topics, 'users' => $users, 'targets' => $targets, @@ -2038,7 +2371,7 @@ App::post('/v1/messaging/messages/push') ->label('scope', 'messages.write') ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'messaging') - ->label('sdk.method', 'createPushNotification') + ->label('sdk.method', 'createPushMessage') ->label('sdk.description', '/docs/references/messaging/create-push-notification.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) @@ -2057,60 +2390,50 @@ App::post('/v1/messaging/messages/push') ->param('color', '', new Text(256), 'Color for push notification. Available only for Android Platform.', true) ->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true) ->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true) - ->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) + ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, string $description, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $deliveryTime, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, string $description, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { throw new Exception(Exception::MESSAGE_MISSING_TARGET); } - $pushData = [ - 'title' => $title, - 'body' => $body, - ]; + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); - if (!is_null($data)) { - $pushData['data'] = $data; + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH . ' ' . $targetDocument->getId()); + } } - if ($action) { - $pushData['action'] = $action; - } + $pushData = []; - if ($icon) { - $pushData['icon'] = $icon; - } + $keys = ['title', 'body', 'data', 'action', 'icon', 'sound', 'color', 'tag', 'badge']; - if ($sound) { - $pushData['sound'] = $sound; - } - - if ($color) { - $pushData['color'] = $color; - } - - if ($tag) { - $pushData['tag'] = $tag; - } - - if ($badge) { - $pushData['badge'] = $badge; + foreach ($keys as $key) { + if (!empty($$key)) { + $pushData[$key] = $$key; + } } $message = $dbForProject->createDocument('messages', new Document([ '$id' => $messageId, + 'providerType' => MESSAGE_TYPE_PUSH, 'topics' => $topics, 'users' => $users, 'targets' => $targets, 'description' => $description, - 'deliveryTime' => $deliveryTime, + 'scheduledAt' => $scheduledAt, 'data' => $pushData, 'status' => $status, ])); @@ -2302,15 +2625,15 @@ App::patch('/v1/messaging/messages/email/:messageId') ->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. Value must be either draft or processing.', true) + ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) + ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $subject, string $description, string $content, string $status, bool $html, ?string $deliveryTime, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $subject, string $description, string $content, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -2321,7 +2644,7 @@ App::patch('/v1/messaging/messages/email/:messageId') throw new Exception(Exception::MESSAGE_ALREADY_SENT); } - if (!is_null($message->getAttribute('deliveryTime')) && $message->getAttribute('deliveryTime') < new \DateTime()) { + if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) { throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); } @@ -2334,6 +2657,18 @@ App::patch('/v1/messaging/messages/email/:messageId') } if (!\is_null($targets)) { + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_EMAIL) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL . ' ' . $targetDocument->getId()); + } + } + $message->setAttribute('targets', $targets); } @@ -2361,8 +2696,8 @@ App::patch('/v1/messaging/messages/email/:messageId') $message->setAttribute('status', $status); } - if (!is_null($deliveryTime)) { - $message->setAttribute('deliveryTime', $deliveryTime); + if (!is_null($scheduledAt)) { + $message->setAttribute('scheduledAt', $scheduledAt); } $message = $dbForProject->updateDocument('messages', $message->getId(), $message); @@ -2401,14 +2736,14 @@ App::patch('/v1/messaging/messages/sms/:messageId') ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) ->param('description', '', new Text(256), 'Description for Message.', true) ->param('content', '', new Text(64230), 'Email Content.', true) - ->param('status', '', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) + ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $description, string $content, string $status, ?string $deliveryTime, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $description, string $content, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -2419,7 +2754,7 @@ App::patch('/v1/messaging/messages/sms/:messageId') throw new Exception(Exception::MESSAGE_ALREADY_SENT); } - if (!is_null($message->getAttribute('deliveryTime')) && $message->getAttribute('deliveryTime') < new \DateTime()) { + if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) { throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); } @@ -2432,6 +2767,18 @@ App::patch('/v1/messaging/messages/sms/:messageId') } if (!\is_null($targets)) { + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_SMS) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS . ' ' . $targetDocument->getId()); + } + } + $message->setAttribute('targets', $targets); } @@ -2451,8 +2798,8 @@ App::patch('/v1/messaging/messages/sms/:messageId') $message->setAttribute('description', $description); } - if (!is_null($deliveryTime)) { - $message->setAttribute('deliveryTime', $deliveryTime); + if (!is_null($scheduledAt)) { + $message->setAttribute('scheduledAt', $scheduledAt); } $message = $dbForProject->updateDocument('messages', $message->getId(), $message); @@ -2498,14 +2845,15 @@ App::patch('/v1/messaging/messages/push/:messageId') ->param('sound', '', new Text(256), 'Sound for push notification. Available only for Android and IOS Platform.', true) ->param('color', '', new Text(256), 'Color for push notification. Available only for Android Platform.', true) ->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true) - ->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true) ->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true) - ->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) + ->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true) + ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $description, string $title, string $body, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $deliveryTime, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $description, string $title, string $body, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -2516,7 +2864,7 @@ App::patch('/v1/messaging/messages/push/:messageId') throw new Exception(Exception::MESSAGE_ALREADY_SENT); } - if (!is_null($message->getAttribute('deliveryTime')) && $message->getAttribute('deliveryTime') < new \DateTime()) { + if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) { throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); } @@ -2529,6 +2877,18 @@ App::patch('/v1/messaging/messages/push/:messageId') } if (!\is_null($targets)) { + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH . ' ' . $targetDocument->getId()); + } + } + $message->setAttribute('targets', $targets); } @@ -2580,8 +2940,8 @@ App::patch('/v1/messaging/messages/push/:messageId') $message->setAttribute('description', $description); } - if (!is_null($deliveryTime)) { - $message->setAttribute('deliveryTime', $deliveryTime); + if (!is_null($scheduledAt)) { + $message->setAttribute('scheduledAt', $scheduledAt); } $message = $dbForProject->updateDocument('messages', $message->getId(), $message); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index d9968c83a7..8a71f33d8b 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -99,6 +99,42 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'memberships' => null, 'search' => implode(' ', [$userId, $email, $phone, $name]), ])); + + if ($email) { + try { + $target = $dbForProject->createDocument('targets', new Document([ + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => 'email', + 'identifier' => $email, + ])); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]); + } catch (Duplicate) { + $existingTarget = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ]); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]); + } + } + + if ($phone) { + try { + $target = $dbForProject->createDocument('targets', new Document([ + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => 'sms', + 'identifier' => $phone, + ])); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]); + } catch (Duplicate) { + $existingTarget = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$phone]), + ]); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]); + } + } + + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate $th) { throw new Exception(Exception::USER_ALREADY_EXISTS); } @@ -396,18 +432,19 @@ App::post('/v1/users/:userId/targets') ->label('sdk.response.model', Response::MODEL_TARGET) ->param('targetId', '', new CustomId(), 'Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('userId', '', new UID(), 'User ID.') - ->param('providerType', '', new WhiteList(['email', 'sms', 'push']), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.') + ->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.') ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)') ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true) + ->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true) ->inject('queueForEvents') ->inject('response') ->inject('dbForProject') - ->action(function (string $targetId, string $userId, string $providerType, string $identifier, string $providerId, Event $queueForEvents, Response $response, Database $dbForProject) { + ->action(function (string $targetId, string $userId, string $providerType, string $identifier, string $providerId, string $name, Event $queueForEvents, Response $response, Database $dbForProject) { $targetId = $targetId == 'unique()' ? ID::unique() : $targetId; $provider = new Document(); - if ($providerType === 'push') { + if ($providerType === MESSAGE_TYPE_PUSH) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -415,6 +452,25 @@ App::post('/v1/users/:userId/targets') } } + switch ($providerType) { + case 'email': + $validator = new Email(); + if (!$validator->isValid($identifier)) { + throw new Exception(Exception::GENERAL_INVALID_EMAIL); + } + break; + case MESSAGE_TYPE_SMS: + $validator = new Phone(); + if (!$validator->isValid($identifier)) { + throw new Exception(Exception::GENERAL_INVALID_PHONE); + } + break; + case MESSAGE_TYPE_PUSH: + break; + default: + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + $user = $dbForProject->getDocument('users', $userId); if ($user->isEmpty()) { @@ -436,6 +492,7 @@ App::post('/v1/users/:userId/targets') 'userId' => $userId, 'userInternalId' => $user->getInternalId(), 'identifier' => $identifier, + 'name' => ($name !== '') ? $name : null, ])); } catch (Duplicate) { throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); @@ -782,7 +839,7 @@ App::get('/v1/users/:userId/targets') if ($cursor) { $targetId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + $cursorDocument = $dbForProject->getDocument('targets', $targetId); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Target '{$targetId}' for the 'cursor' value not found."); @@ -1100,6 +1157,16 @@ App::patch('/v1/users/:userId/email') throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } + $target = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ]); + + if ($target instanceof Document && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + $oldEmail = $user->getAttribute('email'); + $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) @@ -1108,6 +1175,15 @@ App::patch('/v1/users/:userId/email') try { $user = $dbForProject->updateDocument('users', $user->getId(), $user); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldEmail, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate $th) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -1145,13 +1221,32 @@ App::patch('/v1/users/:userId/phone') throw new Exception(Exception::USER_NOT_FOUND); } + $oldPhone = $user->getAttribute('phone'); + $user ->setAttribute('phone', $number) ->setAttribute('phoneVerification', false) ; + $target = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$number]), + ]); + + if ($target instanceof Document && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + try { $user = $dbForProject->updateDocument('users', $user->getId(), $user); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldPhone, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number)); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate $th) { throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS); } @@ -1247,12 +1342,13 @@ App::patch('/v1/users/:userId/targets/:targetId') ->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. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true) ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)', true) + ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true) + ->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true) ->inject('queueForEvents') ->inject('response') ->inject('dbForProject') - ->action(function (string $userId, string $targetId, string $providerId, string $identifier, Event $queueForEvents, Response $response, Database $dbForProject) { + ->action(function (string $userId, string $targetId, string $identifier, string $providerId, string $name, Event $queueForEvents, Response $response, Database $dbForProject) { $user = $dbForProject->getDocument('users', $userId); if ($user->isEmpty()) { @@ -1270,6 +1366,27 @@ App::patch('/v1/users/:userId/targets/:targetId') } if ($identifier) { + $providerType = $target->getAttribute('providerType'); + + switch ($providerType) { + case 'email': + $validator = new Email(); + if (!$validator->isValid($identifier)) { + throw new Exception(Exception::GENERAL_INVALID_EMAIL); + } + break; + case MESSAGE_TYPE_SMS: + $validator = new Phone(); + if (!$validator->isValid($identifier)) { + throw new Exception(Exception::GENERAL_INVALID_PHONE); + } + break; + case MESSAGE_TYPE_PUSH: + break; + default: + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + $target->setAttribute('identifier', $identifier); } @@ -1280,10 +1397,18 @@ App::patch('/v1/users/:userId/targets/:targetId') throw new Exception(Exception::PROVIDER_NOT_FOUND); } + if ($provider->getAttribute('type') !== $target->getAttribute('providerType')) { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + $target->setAttribute('providerId', $provider->getId()); $target->setAttribute('providerInternalId', $provider->getInternalId()); } + if ($name) { + $target->setAttribute('name', $name); + } + $target = $dbForProject->updateDocument('targets', $target->getId(), $target); $dbForProject->deleteCachedDocument('users', $user->getId()); diff --git a/app/init.php b/app/init.php index 6f61b955f3..4dfdcaf108 100644 --- a/app/init.php +++ b/app/init.php @@ -190,6 +190,10 @@ const MAX_OUTPUT_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB // Function headers const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host']; const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length']; +// Message types +const MESSAGE_TYPE_EMAIL = 'email'; +const MESSAGE_TYPE_SMS = 'sms'; +const MESSAGE_TYPE_PUSH = 'push'; // Usage metrics const METRIC_TEAMS = 'teams'; const METRIC_USERS = 'users'; @@ -605,13 +609,14 @@ Database::addFilter( ]; $data = \json_decode($message->getAttribute('data', []), true); + $providerType = $message->getAttribute('providerType', ''); - if (\array_key_exists('subject', $data)) { - $searchValues = \array_merge($searchValues, [$data['subject'], 'email']); - } elseif (\array_key_exists('content', $data)) { - $searchValues = \array_merge($searchValues, [$data['content'], 'sms']); + if ($providerType === MESSAGE_TYPE_EMAIL) { + $searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]); + } elseif ($providerType === MESSAGE_TYPE_SMS) { + $searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]); } else { - $searchValues = \array_merge($searchValues, [$data['title'], 'push']); + $searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]); } $search = \implode(' ', \array_filter($searchValues)); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 898b46b3a5..252b5b6bd7 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -521,14 +521,20 @@ services: environment: - _APP_ENV - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER - _APP_REDIS_PASS - - _APP_SMS_PROVIDER - - _APP_SMS_FROM + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG + - _APP_SMS_FROM + - _APP_SMS_PROVIDER appwrite-worker-migrations: image: /: diff --git a/composer.lock b/composer.lock index afa71aaf2b..16f44a6357 100644 --- a/composer.lock +++ b/composer.lock @@ -156,11 +156,11 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.13.1", + "version": "0.13.2", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "b584d19cdcd82737d0ee5c34d23de791f5ed3610" + "reference": "214a37c2c66e0f2bc9c30fdfde66955d9fd084a1" }, "require": { "php": ">=8.0", @@ -195,7 +195,7 @@ "php", "runtimes" ], - "time": "2023-10-16T15:39:53+00:00" + "time": "2023-11-22T15:36:00+00:00" }, { "name": "chillerlan/php-qrcode", @@ -1465,7 +1465,7 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", @@ -1512,7 +1512,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" }, "funding": [ { @@ -2217,16 +2217,16 @@ }, { "name": "utopia-php/logger", - "version": "0.3.1", + "version": "0.3.2", "source": { "type": "git", "url": "https://github.com/utopia-php/logger.git", - "reference": "de623f1ec1c672c795d113dd25c5bf212f7ef4fc" + "reference": "ba763c10688fe2ed715ad2bed3f13d18dfec6253" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/logger/zipball/de623f1ec1c672c795d113dd25c5bf212f7ef4fc", - "reference": "de623f1ec1c672c795d113dd25c5bf212f7ef4fc", + "url": "https://api.github.com/repos/utopia-php/logger/zipball/ba763c10688fe2ed715ad2bed3f13d18dfec6253", + "reference": "ba763c10688fe2ed715ad2bed3f13d18dfec6253", "shasum": "" }, "require": { @@ -2264,9 +2264,9 @@ ], "support": { "issues": "https://github.com/utopia-php/logger/issues", - "source": "https://github.com/utopia-php/logger/tree/0.3.1" + "source": "https://github.com/utopia-php/logger/tree/0.3.2" }, - "time": "2023-02-10T15:52:50+00:00" + "time": "2023-11-22T14:45:43+00:00" }, { "name": "utopia-php/messaging", @@ -3136,16 +3136,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.35.2", + "version": "0.35.3", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "2dfe0430a64ffd2a07078d83b20144b871acac3b" + "reference": "4c431d5324a8f8cd2cab9a5515c170a5b427d44c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/2dfe0430a64ffd2a07078d83b20144b871acac3b", - "reference": "2dfe0430a64ffd2a07078d83b20144b871acac3b", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/4c431d5324a8f8cd2cab9a5515c170a5b427d44c", + "reference": "4c431d5324a8f8cd2cab9a5515c170a5b427d44c", "shasum": "" }, "require": { @@ -3181,9 +3181,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.35.2" + "source": "https://github.com/appwrite/sdk-generator/tree/0.35.3" }, - "time": "2023-09-14T14:59:50+00:00" + "time": "2023-11-12T05:56:27+00:00" }, { "name": "doctrine/deprecations", @@ -3890,16 +3890,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.2", + "version": "1.24.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bcad8d995980440892759db0c32acae7c8e79442" + "reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442", - "reference": "bcad8d995980440892759db0c32acae7c8e79442", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6bd0c26f3786cd9b7c359675cb789e35a8e07496", + "reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496", "shasum": "" }, "require": { @@ -3931,9 +3931,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.4" }, - "time": "2023-09-26T12:28:12+00:00" + "time": "2023-11-26T18:29:22+00:00" }, { "name": "phpunit/php-code-coverage", @@ -5676,16 +5676,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -5714,7 +5714,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -5722,30 +5722,31 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" }, { "name": "twig/twig", - "version": "v3.7.1", + "version": "v3.8.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554" + "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/a0ce373a0ca3bf6c64b9e3e2124aca502ba39554", - "reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", + "reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d", "shasum": "" }, "require": { "php": ">=7.2.5", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3" + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php80": "^1.22" }, "require-dev": { "psr/container": "^1.0|^2.0", - "symfony/phpunit-bridge": "^5.4.9|^6.3" + "symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0" }, "type": "library", "autoload": { @@ -5781,7 +5782,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.7.1" + "source": "https://github.com/twigphp/Twig/tree/v3.8.0" }, "funding": [ { @@ -5793,7 +5794,7 @@ "type": "tidelift" } ], - "time": "2023-08-28T11:09:02+00:00" + "time": "2023-11-21T18:54:41+00:00" } ], "aliases": [], diff --git a/src/Appwrite/Event/Messaging.php b/src/Appwrite/Event/Messaging.php index 62d41f8c3b..9201799355 100644 --- a/src/Appwrite/Event/Messaging.php +++ b/src/Appwrite/Event/Messaging.php @@ -11,7 +11,7 @@ class Messaging extends Event protected ?string $messageId = null; protected ?Document $message = null; protected ?array $recipients = null; - protected ?string $deliveryTime = null; + protected ?string $scheduledAt = null; protected ?string $providerType = null; @@ -117,14 +117,14 @@ class Messaging extends Event } /** - * Sets Delivery time for the messaging event. + * Sets Scheduled delivery time for the messaging event. * - * @param string $deliveryTime + * @param string $scheduledAt * @return self */ - public function setDeliveryTime(string $deliveryTime): self + public function setScheduledAt(string $scheduledAt): self { - $this->deliveryTime = $deliveryTime; + $this->scheduledAt = $scheduledAt; return $this; } @@ -134,9 +134,9 @@ class Messaging extends Event * * @return string */ - public function getDeliveryTime(): string + public function getScheduledAt(): string { - return $this->deliveryTime; + return $this->scheduledAt; } /** diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 3869444752..5c928569aa 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -55,6 +55,8 @@ class Exception extends \Exception public const GENERAL_CODES_DISABLED = 'general_codes_disabled'; public const GENERAL_USAGE_DISABLED = 'general_usage_disabled'; public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented'; + public const GENERAL_INVALID_EMAIL = 'general_invalid_email'; + public const GENERAL_INVALID_PHONE = 'general_invalid_phone'; /** Users */ public const USER_COUNT_EXCEEDED = 'user_count_exceeded'; @@ -254,6 +256,10 @@ class Exception extends \Exception public const MESSAGE_MISSING_TARGET = 'message_missing_target'; public const MESSAGE_ALREADY_SENT = 'message_already_sent'; public const MESSAGE_ALREADY_SCHEDULED = 'message_already_scheduled'; + public const MESSAGE_TARGET_NOT_EMAIL = 'message_target_not_email'; + public const MESSAGE_TARGET_NOT_SMS = 'message_target_not_sms'; + public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push'; + protected string $type = ''; protected array $errors = []; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 74365bad86..9a9e965f37 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -556,6 +556,11 @@ class Deletes extends Action $this->deleteByGroup('identities', [ Query::equal('userInternalId', [$userInternalId]) ], $dbForProject); + + // Delete targets + $this->deleteByGroup('targets', [ + Query::equal('userInternalId', [$userInternalId]) + ], $dbForProject); } /** diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 299ef4526f..8f945e947f 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -17,7 +17,7 @@ use Utopia\Messaging\Adapters\SMS as SMSAdapter; use Utopia\Messaging\Adapters\SMS\Mock; use Utopia\Messaging\Adapters\SMS\Msg91; use Utopia\Messaging\Adapters\SMS\Telesign; -use Utopia\Messaging\Adapters\SMS\TextMagic; +use Utopia\Messaging\Adapters\SMS\Textmagic; use Utopia\Messaging\Adapters\SMS\Twilio; use Utopia\Messaging\Adapters\SMS\Vonage; use Utopia\Messaging\Adapters\Push as PushAdapter; @@ -67,7 +67,7 @@ class Messaging extends Action } if (!\is_null($payload['message']) && !\is_null($payload['recipients'])) { - if ($payload['providerType'] === 'SMS') { + if ($payload['providerType'] === MESSAGE_TYPE_SMS) { $this->processInternalSMSMessage(new Document($payload['message']), $payload['recipients']); } } else { @@ -91,14 +91,16 @@ class Messaging extends Action if (\count($topicsId) > 0) { $topics = $dbForProject->find('topics', [Query::equal('$id', $topicsId)]); foreach ($topics as $topic) { - $recipients = \array_merge($recipients, $topic->getAttribute('targets')); + $targets = \array_filter($topic->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType')); + $recipients = \array_merge($recipients, $targets); } } if (\count($usersId) > 0) { $users = $dbForProject->find('users', [Query::equal('$id', $usersId)]); foreach ($users as $user) { - $recipients = \array_merge($recipients, $user->getAttribute('targets')); + $targets = \array_filter($user->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType')); + $recipients = \array_merge($recipients, $targets); } } @@ -107,7 +109,7 @@ class Messaging extends Action $recipients = \array_merge($recipients, $targets); } - $internalProvider = $dbForProject->findOne('providers', [ + $primaryProvider = $dbForProject->findOne('providers', [ Query::equal('enabled', [true]), Query::equal('type', [$recipients[0]->getAttribute('providerType')]), ]); @@ -124,36 +126,42 @@ class Messaging extends Action foreach ($recipients as $recipient) { $providerId = $recipient->getAttribute('providerId'); - if (!$providerId) { - $providerId = $internalProvider->getId(); + if (!$providerId && $primaryProvider instanceof Document && !$primaryProvider->isEmpty()) { + $providerId = $primaryProvider->getId(); } - if (!isset($identifiersByProviderId[$providerId])) { - $identifiersByProviderId[$providerId] = []; + if ($providerId) { + if (!isset($identifiersByProviderId[$providerId])) { + $identifiersByProviderId[$providerId] = []; + } + $identifiersByProviderId[$providerId][] = $recipient->getAttribute('identifier'); } - $identifiersByProviderId[$providerId][] = $recipient->getAttribute('identifier'); } /** * @var array[] $results */ - $results = batch(\array_map(function ($providerId) use ($identifiersByProviderId, $providers, $internalProvider, $message, $dbForProject) { - return function () use ($providerId, $identifiersByProviderId, $providers, $internalProvider, $message, $dbForProject) { + $results = batch(\array_map(function ($providerId) use ($identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) { + return function () use ($providerId, $identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) { $provider = new Document(); - if ($internalProvider->getId() === $providerId) { - $provider = $internalProvider; + if ($primaryProvider->getId() === $providerId) { + $provider = $primaryProvider; } else { - $provider = $dbForProject->getDocument('providers', $providerId); + $provider = $dbForProject->getDocument('providers', $providerId, [Query::equal('enabled', [true])]); + + if ($provider->isEmpty()) { + $provider = $primaryProvider; + } } $providers[] = $provider; $identifiers = $identifiersByProviderId[$providerId]; $adapter = match ($provider->getAttribute('type')) { - 'sms' => $this->sms($provider), - 'push' => $this->push($provider), - 'email' => $this->email($provider), + MESSAGE_TYPE_SMS => $this->sms($provider), + MESSAGE_TYPE_PUSH => $this->push($provider), + MESSAGE_TYPE_EMAIL => $this->email($provider), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; @@ -169,9 +177,9 @@ class Messaging extends Action $messageData->setAttribute('to', $batch); $data = match ($provider->getAttribute('type')) { - 'sms' => $this->buildSMSMessage($messageData, $provider), - 'push' => $this->buildPushMessage($messageData), - 'email' => $this->buildEmailMessage($messageData, $provider), + MESSAGE_TYPE_SMS => $this->buildSMSMessage($messageData, $provider), + MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData), + MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($messageData, $provider), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; @@ -241,7 +249,7 @@ class Messaging extends Action $provider = new Document([ '$id' => ID::unique(), 'provider' => $host, - 'type' => 'sms', + 'type' => MESSAGE_TYPE_SMS, 'name' => 'Internal SMS', 'enabled' => true, 'credentials' => match ($host) { @@ -303,7 +311,7 @@ class Messaging extends Action return match ($provider->getAttribute('provider')) { 'mock' => new Mock('username', 'password'), 'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']), - 'textmagic' => new TextMagic($credentials['username'], $credentials['apiKey']), + 'textmagic' => new Textmagic($credentials['username'], $credentials['apiKey']), 'telesign' => new Telesign($credentials['username'], $credentials['password']), 'msg91' => new Msg91($credentials['senderId'], $credentials['authKey']), 'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']), diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Subscribers.php b/src/Appwrite/Utopia/Database/Validator/Queries/Subscribers.php index 55bb455903..05e08a75a7 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Subscribers.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Subscribers.php @@ -6,7 +6,9 @@ class Subscribers extends Base { public const ALLOWED_ATTRIBUTES = [ 'targetId', - 'topicId' + 'topicId', + 'userId', + 'providerType' ]; /** diff --git a/src/Appwrite/Utopia/Response/Model/Message.php b/src/Appwrite/Utopia/Response/Model/Message.php index 27c70d7073..791c87933f 100644 --- a/src/Appwrite/Utopia/Response/Model/Message.php +++ b/src/Appwrite/Utopia/Response/Model/Message.php @@ -5,8 +5,9 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; use Utopia\Database\DateTime; +use Utopia\Database\Document as DatabaseDocument; -class Message extends Any +class Message extends Model { public function __construct() { @@ -29,6 +30,12 @@ class Message extends Any 'default' => '', 'example' => self::TYPE_DATETIME_EXAMPLE, ]) + ->addRule('providerType', [ + 'type' => self::TYPE_STRING, + 'description' => 'Message provider type.', + 'default' => '', + 'example' => MESSAGE_TYPE_EMAIL, + ]) ->addRule('topics', [ 'type' => self::TYPE_STRING, 'description' => 'Topic IDs set as recipients.', @@ -50,7 +57,7 @@ class Message extends Any 'array' => true, 'example' => ['5e5ea5c16897e'], ]) - ->addRule('deliveryTime', [ + ->addRule('scheduledAt', [ 'type' => self::TYPE_DATETIME, 'description' => 'The scheduled time for message.', 'required' => false, @@ -91,7 +98,7 @@ class Message extends Any 'type' => self::TYPE_STRING, 'description' => 'Status of delivery.', 'default' => 'processing', - 'example' => 'Message status can be one of the following: processing, sent, failed.', + 'example' => 'Message status can be one of the following: processing, sent, cancelled, failed.', ]) ->addRule('description', [ 'type' => self::TYPE_STRING, diff --git a/src/Appwrite/Utopia/Response/Model/Provider.php b/src/Appwrite/Utopia/Response/Model/Provider.php index f8a0514020..d3de061aab 100644 --- a/src/Appwrite/Utopia/Response/Model/Provider.php +++ b/src/Appwrite/Utopia/Response/Model/Provider.php @@ -50,7 +50,7 @@ class Provider extends Model 'type' => self::TYPE_STRING, 'description' => 'Type of provider.', 'default' => '', - 'example' => 'sms', + 'example' => MESSAGE_TYPE_SMS, ]) ->addRule('credentials', [ 'type' => self::TYPE_JSON, diff --git a/src/Appwrite/Utopia/Response/Model/Subscriber.php b/src/Appwrite/Utopia/Response/Model/Subscriber.php index 5080b44333..8c3a4c7a49 100644 --- a/src/Appwrite/Utopia/Response/Model/Subscriber.php +++ b/src/Appwrite/Utopia/Response/Model/Subscriber.php @@ -34,9 +34,24 @@ class Subscriber extends Model 'default' => '', 'example' => '259125845563242502', ]) + ->addRule('target', [ + 'type' => Response::MODEL_TARGET, + 'description' => 'Target.', + 'default' => [], + 'example' => [ + '$id' => '259125845563242502', + '$createdAt' => self::TYPE_DATETIME_EXAMPLE, + '$updatedAt' => self::TYPE_DATETIME_EXAMPLE, + 'providerType' => 'email', + 'providerId' => '259125845563242502', + 'name' => 'ageon-app-email', + 'identifier' => 'random-mail@email.org', + 'userId' => '5e5ea5c16897e', + ], + ]) ->addRule('userId', [ 'type' => self::TYPE_STRING, - 'description' => 'User ID.', + 'description' => 'Topic ID.', 'default' => '', 'example' => '5e5ea5c16897e', ]) @@ -51,6 +66,12 @@ class Subscriber extends Model 'description' => 'Topic ID.', 'default' => '', 'example' => '259125845563242502', + ]) + ->addRule('providerType', [ + 'type' => self::TYPE_STRING, + 'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.', + 'default' => '', + 'example' => MESSAGE_TYPE_EMAIL, ]); } diff --git a/src/Appwrite/Utopia/Response/Model/Target.php b/src/Appwrite/Utopia/Response/Model/Target.php index f6346f409a..d180b6c4c4 100644 --- a/src/Appwrite/Utopia/Response/Model/Target.php +++ b/src/Appwrite/Utopia/Response/Model/Target.php @@ -28,6 +28,12 @@ class Target extends Model 'default' => '', 'example' => self::TYPE_DATETIME_EXAMPLE, ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'Target Name.', + 'default' => '', + 'example' => 'Aegon apple token', + ]) ->addRule('userId', [ 'type' => self::TYPE_STRING, 'description' => 'User ID.', @@ -45,7 +51,7 @@ class Target extends Model 'type' => self::TYPE_STRING, 'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.', 'default' => '', - 'example' => 'email', + 'example' => MESSAGE_TYPE_EMAIL, ]) ->addRule('identifier', [ 'type' => self::TYPE_STRING, diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 8655395dc2..2854d0bf42 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -1830,8 +1830,8 @@ trait Base } }'; case self::$CREATE_TEXTMAGIC_PROVIDER: - return 'mutation createTextMagicProvider($providerId: String!, $name: String!, $from: String!, $username: String!, $apiKey: String!) { - messagingCreateTextMagicProvider(providerId: $providerId, name: $name, from: $from, username: $username, apiKey: $apiKey) { + return 'mutation createTextmagicProvider($providerId: String!, $name: String!, $from: String!, $username: String!, $apiKey: String!) { + messagingCreateTextmagicProvider(providerId: $providerId, name: $name, from: $from, username: $username, apiKey: $apiKey) { _id name provider @@ -1944,8 +1944,8 @@ trait Base } }'; case self::$UPDATE_TEXTMAGIC_PROVIDER: - return 'mutation updateTextMagicProvider($providerId: String!, $name: String!, $username: String!, $apiKey: String!) { - messagingUpdateTextMagicProvider(providerId: $providerId, name: $name, username: $username, apiKey: $apiKey) { + return 'mutation updateTextmagicProvider($providerId: String!, $name: String!, $username: String!, $apiKey: String!) { + messagingUpdateTextmagicProvider(providerId: $providerId, name: $name, username: $username, apiKey: $apiKey) { _id name provider @@ -2044,9 +2044,16 @@ trait Base return 'mutation createSubscriber($subscriberId: String!, $targetId: String!, $topicId: String!) { messagingCreateSubscriber(subscriberId: $subscriberId, targetId: $targetId, topicId: $topicId) { _id - userId targetId topicId + userName + target { + _id + userId + name + providerType + identifier + } } }'; case self::$LIST_SUBSCRIBERS: @@ -2055,9 +2062,16 @@ trait Base total subscribers { _id - userId targetId topicId + userName + target { + _id + userId + name + providerType + identifier + } } } }'; @@ -2065,9 +2079,16 @@ trait Base return 'query getSubscriber($topicId: String!, $subscriberId: String!) { messagingGetSubscriber(topicId: $topicId, subscriberId: $subscriberId) { _id - userId targetId topicId + userName + target { + _id + userId + name + providerType + identifier + } } }'; case self::$DELETE_SUBSCRIBER: @@ -2077,13 +2098,13 @@ trait Base } }'; case self::$CREATE_EMAIL: - return 'mutation createEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String!, $content: String!, $status: String, $description: String, $html: Boolean, $deliveryTime: String) { - messagingCreateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, deliveryTime: $deliveryTime) { + return 'mutation createEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String!, $content: String!, $status: String, $description: String, $html: Boolean, $scheduledAt: String) { + messagingCreateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) { _id topics users targets - deliveryTime + scheduledAt deliveredAt deliveryErrors deliveredTotal @@ -2092,13 +2113,13 @@ trait Base } }'; case self::$CREATE_SMS: - return 'mutation createSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String!, $status: String, $description: String, $deliveryTime: String) { - messagingCreateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, deliveryTime: $deliveryTime) { + return 'mutation createSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String!, $status: String, $description: String, $scheduledAt: String) { + messagingCreateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, scheduledAt: $scheduledAt) { _id topics users targets - deliveryTime + scheduledAt deliveredAt deliveryErrors deliveredTotal @@ -2107,13 +2128,13 @@ trait Base } }'; case self::$CREATE_PUSH_NOTIFICATION: - return 'mutation createPushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String!, $body: String!, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $deliveryTime: String) { - messagingCreatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, deliveryTime: $deliveryTime) { + return 'mutation createPushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String!, $body: String!, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $scheduledAt: String) { + messagingCreatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, scheduledAt: $scheduledAt) { _id topics users targets - deliveryTime + scheduledAt deliveredAt deliveryErrors deliveredTotal @@ -2127,10 +2148,11 @@ trait Base total messages { _id + providerType topics users targets - deliveryTime + scheduledAt deliveredAt deliveryErrors deliveredTotal @@ -2143,10 +2165,11 @@ trait Base return 'query getMessage($messageId: String!) { messagingGetMessage(messageId: $messageId) { _id + providerType topics users targets - deliveryTime + scheduledAt deliveredAt deliveryErrors deliveredTotal @@ -2155,13 +2178,13 @@ trait Base } }'; case self::$UPDATE_EMAIL: - return 'mutation updateEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String, $content: String, $status: String, $description: String, $html: Boolean, $deliveryTime: String) { - messagingUpdateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, deliveryTime: $deliveryTime) { + return 'mutation updateEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String, $content: String, $status: String, $description: String, $html: Boolean, $scheduledAt: String) { + messagingUpdateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) { _id topics users targets - deliveryTime + scheduledAt deliveredAt deliveryErrors deliveredTotal @@ -2170,13 +2193,13 @@ trait Base } }'; case self::$UPDATE_SMS: - return 'mutation updateSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String, $status: String, $description: String, $deliveryTime: String) { - messagingUpdateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, deliveryTime: $deliveryTime) { + return 'mutation updateSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String, $status: String, $description: String, $scheduledAt: String) { + messagingUpdateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, scheduledAt: $scheduledAt) { _id topics users targets - deliveryTime + scheduledAt deliveredAt deliveryErrors deliveredTotal @@ -2185,13 +2208,13 @@ trait Base } }'; case self::$UPDATE_PUSH_NOTIFICATION: - return 'mutation updatePushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String, $body: String, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $deliveryTime: String) { - messagingUpdatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, deliveryTime: $deliveryTime) { + return 'mutation updatePushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String, $body: String, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $scheduledAt: String) { + messagingUpdatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, scheduledAt: $scheduledAt) { _id topics users targets - deliveryTime + scheduledAt deliveredAt deliveryErrors deliveredTotal @@ -2451,7 +2474,7 @@ trait Base protected string $stdout = ''; protected string $stderr = ''; - protected function packageCode($folder) + protected function packageCode($folder): void { Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); } diff --git a/tests/e2e/Services/GraphQL/MessagingTest.php b/tests/e2e/Services/GraphQL/MessagingTest.php index 27b2b52cd3..d1a084cfc1 100644 --- a/tests/e2e/Services/GraphQL/MessagingTest.php +++ b/tests/e2e/Services/GraphQL/MessagingTest.php @@ -47,7 +47,7 @@ class MessagingTest extends Scope 'password' => 'my-password', 'from' => '+123456789', ], - 'TextMagic' => [ + 'Textmagic' => [ 'providerId' => ID::unique(), 'name' => 'Textmagic1', 'username' => 'my-username', @@ -134,7 +134,7 @@ class MessagingTest extends Scope 'username' => 'my-username', 'password' => 'my-password', ], - 'TextMagic' => [ + 'Textmagic' => [ 'providerId' => $providers[4]['_id'], 'name' => 'Textmagic2', 'username' => 'my-username', @@ -398,7 +398,7 @@ class MessagingTest extends Scope 'providerType' => 'email', 'userId' => $userId, 'providerId' => $providerId, - 'identifier' => 'token', + 'identifier' => 'random-email@mail.org', ], ]; $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ @@ -409,7 +409,7 @@ class MessagingTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals($userId, $response['body']['data']['usersCreateTarget']['userId']); - $this->assertEquals('token', $response['body']['data']['usersCreateTarget']['identifier']); + $this->assertEquals('random-email@mail.org', $response['body']['data']['usersCreateTarget']['identifier']); $targetId = $response['body']['data']['usersCreateTarget']['_id']; @@ -430,7 +430,7 @@ class MessagingTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['topicId'], $topicId); $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['targetId'], $targetId); - $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['userId'], $userId); + $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['target']['userId'], $userId); return $response['body']['data']['messagingCreateSubscriber']; } @@ -454,9 +454,9 @@ class MessagingTest extends Scope ]), $graphQLPayload); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals($response['body']['data']['messagingListSubscribers']['subscribers'][0]['topicId'], $subscriber['topicId']); - $this->assertEquals($response['body']['data']['messagingListSubscribers']['subscribers'][0]['targetId'], $subscriber['targetId']); - $this->assertEquals($response['body']['data']['messagingListSubscribers']['subscribers'][0]['userId'], $subscriber['userId']); + $this->assertEquals($subscriber['topicId'], $response['body']['data']['messagingListSubscribers']['subscribers'][0]['topicId']); + $this->assertEquals($subscriber['targetId'], $response['body']['data']['messagingListSubscribers']['subscribers'][0]['targetId']); + $this->assertEquals($subscriber['target']['userId'], $response['body']['data']['messagingListSubscribers']['subscribers'][0]['target']['userId']); $this->assertEquals(1, \count($response['body']['data']['messagingListSubscribers']['subscribers'])); } @@ -487,7 +487,7 @@ class MessagingTest extends Scope $this->assertEquals($subscriberId, $response['body']['data']['messagingGetSubscriber']['_id']); $this->assertEquals($topicId, $response['body']['data']['messagingGetSubscriber']['topicId']); $this->assertEquals($subscriber['targetId'], $response['body']['data']['messagingGetSubscriber']['targetId']); - $this->assertEquals($subscriber['userId'], $response['body']['data']['messagingGetSubscriber']['userId']); + $this->assertEquals($subscriber['target']['userId'], $response['body']['data']['messagingGetSubscriber']['target']['userId']); } /** diff --git a/tests/e2e/Services/GraphQL/UsersTest.php b/tests/e2e/Services/GraphQL/UsersTest.php index 59c0c4a805..1dac9123a9 100644 --- a/tests/e2e/Services/GraphQL/UsersTest.php +++ b/tests/e2e/Services/GraphQL/UsersTest.php @@ -80,7 +80,7 @@ class UsersTest extends Scope 'userId' => $user['_id'], 'providerType' => 'email', 'providerId' => $providerId, - 'identifier' => 'identifier', + 'identifier' => 'random-email@mail.org', ] ]; @@ -90,7 +90,7 @@ class UsersTest extends Scope ], $this->getHeaders()), $graphQLPayload); $this->assertEquals(200, $target['headers']['status-code']); - $this->assertEquals('identifier', $target['body']['data']['usersCreateTarget']['identifier']); + $this->assertEquals('random-email@mail.org', $target['body']['data']['usersCreateTarget']['identifier']); return $target['body']['data']['usersCreateTarget']; } @@ -247,7 +247,7 @@ class UsersTest extends Scope $this->assertEquals(200, $targets['headers']['status-code']); $this->assertIsArray($targets['body']['data']['usersListTargets']); - $this->assertCount(1, $targets['body']['data']['usersListTargets']['targets']); + $this->assertCount(2, $targets['body']['data']['usersListTargets']['targets']); } /** @@ -271,7 +271,7 @@ class UsersTest extends Scope ], $this->getHeaders()), $graphQLPayload); $this->assertEquals(200, $target['headers']['status-code']); - $this->assertEquals('identifier', $target['body']['data']['usersGetTarget']['identifier']); + $this->assertEquals('random-email@mail.org', $target['body']['data']['usersGetTarget']['identifier']); } public function testUpdateUserStatus() @@ -470,7 +470,7 @@ class UsersTest extends Scope 'variables' => [ 'userId' => $target['userId'], 'targetId' => $target['_id'], - 'identifier' => 'newidentifier', + 'identifier' => 'random-email1@mail.org', ], ]; @@ -480,7 +480,7 @@ class UsersTest extends Scope ], $this->getHeaders()), $graphQLPayload); $this->assertEquals(200, $target['headers']['status-code']); - $this->assertEquals('newidentifier', $target['body']['data']['usersUpdateTarget']['identifier']); + $this->assertEquals('random-email1@mail.org', $target['body']['data']['usersUpdateTarget']['identifier']); } public function testDeleteUserSessions() diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index 1f09d95522..690e503e77 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -319,7 +319,7 @@ trait MessagingBase 'targetId' => ID::unique(), 'providerType' => 'email', 'providerId' => $provider['body']['$id'], - 'identifier' => 'my-token', + 'identifier' => 'random-email@mail.org', ]); $this->assertEquals(201, $target['headers']['status-code']); @@ -333,7 +333,8 @@ trait MessagingBase ]); $this->assertEquals(201, $response['headers']['status-code']); - $this->assertEquals($target['body']['userId'], $response['body']['userId']); + $this->assertEquals($target['body']['userId'], $response['body']['target']['userId']); + $this->assertEquals($target['body']['providerType'], $response['body']['target']['providerType']); $topic = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['$id'], [ 'content-type' => 'application/json', @@ -350,7 +351,9 @@ trait MessagingBase 'topicId' => $topic['body']['$id'], 'targetId' => $target['body']['$id'], 'userId' => $target['body']['userId'], - 'subscriberId' => $response['body']['$id'] + 'subscriberId' => $response['body']['$id'], + 'identifier' => $target['body']['identifier'], + 'providerType' => $target['body']['providerType'], ]; } @@ -368,7 +371,9 @@ trait MessagingBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals($data['topicId'], $response['body']['topicId']); $this->assertEquals($data['targetId'], $response['body']['targetId']); - $this->assertEquals($data['userId'], $response['body']['userId']); + $this->assertEquals($data['userId'], $response['body']['target']['userId']); + $this->assertEquals($data['providerType'], $response['body']['target']['providerType']); + $this->assertEquals($data['identifier'], $response['body']['target']['identifier']); } /** @@ -384,7 +389,9 @@ trait MessagingBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(1, $response['body']['total']); - $this->assertEquals($data['userId'], $response['body']['subscribers'][0]['userId']); + $this->assertEquals($data['userId'], $response['body']['subscribers'][0]['target']['userId']); + $this->assertEquals($data['providerType'], $response['body']['subscribers'][0]['target']['providerType']); + $this->assertEquals($data['identifier'], $response['body']['subscribers'][0]['target']['identifier']); $this->assertEquals(\count($response['body']['subscribers']), $response['body']['total']); return $data; diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index c9cb17d7b5..7d22f23251 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -23,6 +23,7 @@ trait UsersBase 'password' => 'password', 'name' => 'Cristiano Ronaldo', ], false); + $this->assertEquals($user['headers']['status-code'], 201); // Test empty prefs is object not array $bodyString = $user['body']; @@ -1245,11 +1246,11 @@ trait UsersBase 'targetId' => ID::unique(), 'providerId' => $provider['body']['$id'], 'providerType' => 'email', - 'identifier' => 'my-token', + 'identifier' => 'random-email@mail.org', ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals($provider['body']['$id'], $response['body']['providerId']); - $this->assertEquals('my-token', $response['body']['identifier']); + $this->assertEquals('random-email@mail.org', $response['body']['identifier']); return $response['body']; } @@ -1262,10 +1263,10 @@ trait UsersBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'identifier' => 'my-updated-token', + 'identifier' => 'random-email1@mail.org', ]); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals('my-updated-token', $response['body']['identifier']); + $this->assertEquals('random-email1@mail.org', $response['body']['identifier']); return $response['body']; } @@ -1279,7 +1280,7 @@ trait UsersBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(1, \count($response['body']['targets'])); + $this->assertEquals(2, \count($response['body']['targets'])); } /** @@ -1313,7 +1314,7 @@ trait UsersBase ], $this->getHeaders())); $this->assertEquals(200, $response['headers']['status-code']); - $this->assertEquals(0, $response['body']['total']); + $this->assertEquals(1, $response['body']['total']); } /**