diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 14e1ac5e44..9c9b678302 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -75,8 +75,32 @@ jobs: - name: Run Unit Tests run: docker compose exec appwrite test /usr/src/code/tests/unit - e2e_test: - name: E2E Test + e2e_general_test: + name: E2E General Test + runs-on: ubuntu-latest + needs: setup + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Load Cache + uses: actions/cache@v3 + with: + key: ${{ env.CACHE_KEY }} + path: /tmp/${{ env.IMAGE }}.tar + fail-on-cache-miss: true + + - name: Load and Start Appwrite + run: | + docker load --input /tmp/${{ env.IMAGE }}.tar + docker compose up -d + sleep 10 + + - name: Run General Tests + run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/General --debug + + e2e_service_test: + name: E2E Service Test runs-on: ubuntu-latest needs: setup strategy: @@ -120,4 +144,4 @@ jobs: sleep 10 - name: Run ${{matrix.service}} Tests - run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug \ No newline at end of file + run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index b85bdc0133..daa6e46567 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -876,7 +876,7 @@ App::get('/v1/account/identities') }); App::delete('/v1/account/identities/:identityId') - ->desc('Delete Identity') + ->desc('Delete identity') ->groups(['api', 'account']) ->label('scope', 'account') ->label('event', 'users.[userId].identities.[identityId].delete') @@ -893,7 +893,8 @@ App::delete('/v1/account/identities/:identityId') ->param('identityId', '', new UID(), 'Identity ID.') ->inject('response') ->inject('dbForProject') - ->action(function (string $identityId, Response $response, Database $dbForProject) { + ->inject('queueForEvents') + ->action(function (string $identityId, Response $response, Database $dbForProject, Event $queueForEvents) { $identity = $dbForProject->getDocument('identities', $identityId); @@ -903,6 +904,11 @@ App::delete('/v1/account/identities/:identityId') $dbForProject->deleteDocument('identities', $identityId); + $queueForEvents + ->setParam('userId', $identity->getAttribute('userId')) + ->setParam('identityId', $identity->getId()) + ->setPayload($response->output($identity, Response::MODEL_IDENTITY)); + return $response->noContent(); }); diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 124bc2d219..9a9fdb8b16 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -31,6 +31,7 @@ use Utopia\Database\Validator\UID; use Utopia\Locale\Locale; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; +use Utopia\Validator\Integer; use Utopia\Validator\JSON; use Utopia\Validator\Text; use MaxMind\Db\Reader; @@ -54,21 +55,28 @@ 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.', 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) + ->param('fromName', '', new Text(128), 'Sender Name.', true) + ->param('fromEmail', '', new Email(), 'Sender email address.', true) + ->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name. Reply to name must have reply to email as well.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the reply to field for the mail. Default value is sender email. Reply to email must have reply to name as well.', 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 $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; - $options = []; + $options = [ + 'fromName' => $fromName, + 'fromEmail' => $fromEmail, + ]; - if (!empty($from)) { - $options ['from'] = $from; + if (!empty($replyToName) && !empty($replyToEmail)) { + $options['replyToName'] = $replyToName; + $options['replyToEmail'] = $replyToEmail; } $credentials = []; @@ -137,19 +145,26 @@ 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.', true) ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->param('fromName', '', new Text(128), 'Sender Name.', true) + ->param('fromEmail', '', new Email(), 'Sender email address.', true) + ->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the reply to field for the mail. Default value is sender email.', 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 $apiKey, ?bool $enabled, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; - $options = []; + $options = [ + 'fromName' => $fromName, + 'fromEmail' => $fromEmail, + ]; - if (!empty($from)) { - $options ['from'] = $from; + if (!empty($replyToName) && !empty($replyToEmail)) { + $options['replyToName'] = $replyToName; + $options['replyToEmail'] = $replyToEmail; } $credentials = []; @@ -221,7 +236,7 @@ App::post('/v1/messaging/providers/msg91') $options = []; if (!empty($from)) { - $options ['from'] = $from; + $options['from'] = $from; } $credentials = []; @@ -298,7 +313,7 @@ App::post('/v1/messaging/providers/telesign') $options = []; if (!empty($from)) { - $options ['from'] = $from; + $options['from'] = $from; } $credentials = []; @@ -375,7 +390,7 @@ App::post('/v1/messaging/providers/textmagic') $options = []; if (!empty($from)) { - $options ['from'] = $from; + $options['from'] = $from; } $credentials = []; @@ -452,7 +467,7 @@ App::post('/v1/messaging/providers/twilio') $options = []; if (!empty($from)) { - $options ['from'] = $from; + $options['from'] = $from; } $credentials = []; @@ -529,7 +544,7 @@ App::post('/v1/messaging/providers/vonage') $options = []; if (!empty($from)) { - $options ['from'] = $from; + $options['from'] = $from; } $credentials = []; @@ -593,21 +608,21 @@ 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.', true) + ->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', 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, ?array $serviceAccountJSON, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; $credentials = []; - if (!empty($serverKey)) { - $credentials['serverKey'] = $serverKey; + if (!\is_null($serviceAccountJSON)) { + $credentials['serviceAccountJSON'] = $serviceAccountJSON; } - if ($enabled === true && \array_key_exists('serverKey', $credentials)) { + if ($enabled === true && \array_key_exists('serviceAccountJSON', $credentials)) { $enabled = true; } else { $enabled = false; @@ -888,15 +903,18 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) - ->param('enabled', null, new Boolean(), 'Set as enabled.', true) - ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) - ->param('from', '', new Email(), 'Sender email address.', true) ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) ->param('domain', '', new Text(0), 'Mailgun Domain.', true) + ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->param('fromName', '', new Text(128), 'Sender Name.', true) + ->param('fromEmail', '', new Email(), 'Sender email address.', true) + ->param('replyToName', '', new Text(128), 'Name set in the reply to field for the mail. Default value is sender name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the reply to field for the mail. Default value is sender email.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $from, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -912,12 +930,26 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') $provider->setAttribute('name', $name); } - if (!empty($from)) { - $provider->setAttribute('options', [ - 'from' => $from, - ]); + $options = $provider->getAttribute('options'); + + if (!empty($fromName)) { + $options['fromName'] = $fromName; } + if (!empty($fromEmail)) { + $options['fromEmail'] = $fromEmail; + } + + if (!empty($replyToName)) { + $options['replyToName'] = $replyToName; + } + + if (!empty($replyToEmail)) { + $options['replyToEmail'] = $replyToEmail; + } + + $provider->setAttribute('options', $options); + $credentials = $provider->getAttribute('credentials'); if ($isEuRegion === true || $isEuRegion === false) { @@ -976,11 +1008,14 @@ 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 Email(), 'Sender email address.', true) + ->param('fromName', '', new Text(128), 'Sender Name.', true) + ->param('fromEmail', '', new Email(), 'Sender email address.', true) + ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -996,12 +1031,26 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') $provider->setAttribute('name', $name); } - if (!empty($from)) { - $provider->setAttribute('options', [ - 'from' => $from, - ]); + $options = $provider->getAttribute('options'); + + if (!empty($fromName)) { + $options['fromName'] = $fromName; } + if (!empty($fromEmail)) { + $options['fromEmail'] = $fromEmail; + } + + if (!empty($replyToName)) { + $options['replyToName'] = $replyToName; + } + + if (!empty($replyToEmail)) { + $options['replyToEmail'] = $replyToEmail; + } + + $provider->setAttribute('options', $options); + if (!empty($apiKey)) { $provider->setAttribute('credentials', [ 'apiKey' => $apiKey, @@ -1451,11 +1500,11 @@ App::patch('/v1/messaging/providers/fcm/: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('serverKey', '', new Text(0), 'FCM Server Key.', true) + ->param('serviceAccountJSON', null, new JSON(), 'FCM service account JSON.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, string $serverKey, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, ?array $serviceAccountJSON, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -1471,12 +1520,12 @@ App::patch('/v1/messaging/providers/fcm/:providerId') $provider->setAttribute('name', $name); } - if (!empty($serverKey)) { - $provider->setAttribute('credentials', ['serverKey' => $serverKey]); + if (!\is_null($serviceAccountJSON)) { + $provider->setAttribute('credentials', ['serviceAccountJSON' => $serviceAccountJSON]); } if ($enabled === true || $enabled === false) { - if ($enabled === true && \array_key_exists('serverKey', $provider->getAttribute('credentials'))) { + if ($enabled === true && \array_key_exists('serviceAccountJSON', $provider->getAttribute('credentials'))) { $enabled = true; } else { $enabled = false; @@ -2227,9 +2276,11 @@ App::post('/v1/messaging/messages/email') ->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('subject', '', new Text(998), 'Email Subject.') ->param('content', '', new Text(64230), 'Email Content.') - ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true) - ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true) - ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true) + ->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true) + ->param('cc', [], new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true) + ->param('bcc', [], new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true) ->param('description', '', new Text(256), 'Description for message.', true) ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) @@ -2239,22 +2290,32 @@ App::post('/v1/messaging/messages/email') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, string $description, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { - $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; + ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, 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); + $mergedTargets = \array_merge($targets, $cc, $bcc); - if ($targetDocument->isEmpty()) { - throw new Exception(Exception::USER_TARGET_NOT_FOUND); + if (!empty($mergedTargets)) { + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $mergedTargets), + Query::equal('providerType', [MESSAGE_TYPE_EMAIL]), + Query::limit(\count($mergedTargets)), + ]); + + if (\count($foundTargets) !== \count($mergedTargets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL); } - if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_EMAIL) { - throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL . ' ' . $targetDocument->getId()); + foreach ($foundTargets as $target) { + if ($target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } } } @@ -2269,6 +2330,8 @@ App::post('/v1/messaging/messages/email') 'subject' => $subject, 'content' => $content, 'html' => $html, + 'cc' => $cc, + 'bcc' => $bcc, ], 'status' => $status, ])); @@ -2304,9 +2367,9 @@ App::post('/v1/messaging/messages/sms') ->label('sdk.response.model', Response::MODEL_MESSAGE) ->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('content', '', new Text(64230), 'SMS Content.') - ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true) - ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true) - ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true) + ->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true) ->param('description', '', new Text(256), 'Description for Message.', true) ->param('status', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) @@ -2316,22 +2379,28 @@ App::post('/v1/messaging/messages/sms') ->inject('queueForMessaging') ->inject('response') ->action(function (string $messageId, string $content, array $topics, array $users, array $targets, string $description, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { - $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; + $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); + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $targets), + Query::equal('providerType', [MESSAGE_TYPE_SMS]), + Query::limit(\count($targets)), + ]); - if ($targetDocument->isEmpty()) { + if (\count($foundTargets) !== \count($targets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS); + } + + foreach ($foundTargets as $target) { + if ($target->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([ @@ -2379,9 +2448,9 @@ App::post('/v1/messaging/messages/push') ->param('messageId', '', new CustomId(), 'Message ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('title', '', new Text(256), 'Title for push notification.') ->param('body', '', new Text(64230), 'Body for push notification.') - ->param('topics', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true) - ->param('users', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true) - ->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true) + ->param('topics', [], new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', [], new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true) ->param('description', '', new Text(256), 'Description for Message.', true) ->param('data', null, new JSON(), 'Additional Data for push notification.', true) ->param('action', '', new Text(256), 'Action for push notification.', true) @@ -2398,22 +2467,28 @@ App::post('/v1/messaging/messages/push') ->inject('queueForMessaging') ->inject('response') ->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, string $description, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { - $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; + $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); + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $targets), + Query::equal('providerType', [MESSAGE_TYPE_PUSH]), + Query::limit(\count($targets)), + ]); - if ($targetDocument->isEmpty()) { + if (\count($foundTargets) !== \count($targets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH); + } + + foreach ($foundTargets as $target) { + if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - - if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { - throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH . ' ' . $targetDocument->getId()); - } } $pushData = []; @@ -2619,21 +2694,23 @@ App::patch('/v1/messaging/messages/email/:messageId') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_MESSAGE) ->param('messageId', '', new UID(), 'Message ID.') - ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs.', true) - ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of User IDs.', true) - ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Targets IDs.', true) - ->param('subject', '', new Text(998), 'Email Subject.', true) - ->param('description', '', new Text(256), 'Description for Message.', true) - ->param('content', '', new Text(64230), 'Email Content.', true) - ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) - ->param('html', false, new Boolean(), 'Is content of type HTML', true) + ->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true) + ->param('subject', null, new Text(998), 'Email Subject.', true) + ->param('description', null, new Text(256), 'Description for Message.', true) + ->param('content', null, new Text(64230), 'Email Content.', true) + ->param('status', null, new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('html', null, new Boolean(), 'Is content of type HTML', true) + ->param('cc', null, new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true) + ->param('bcc', null, new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $subject, string $description, string $content, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $description, ?string $content, ?string $status, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -2644,7 +2721,7 @@ App::patch('/v1/messaging/messages/email/:messageId') throw new Exception(Exception::MESSAGE_ALREADY_SENT); } - if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) { + if (!\is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) { throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); } @@ -2656,43 +2733,57 @@ App::patch('/v1/messaging/messages/email/:messageId') $message->setAttribute('users', $users); } - if (!\is_null($targets)) { - foreach ($targets as $target) { - $targetDocument = $dbForProject->getDocument('targets', $target); + if (!\is_null($targets) || !\is_null($cc) || !\is_null($bcc)) { + $mergedTargets = \array_merge(...\array_filter([$targets, $cc, $bcc])); - if ($targetDocument->isEmpty()) { + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $mergedTargets), + Query::equal('providerType', [MESSAGE_TYPE_EMAIL]), + Query::limit(\count($mergedTargets)), + ]); + if (\count($foundTargets) !== \count($mergedTargets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL); + } + foreach ($foundTargets as $target) { + if ($target->isEmpty()) { throw new Exception(Exception::USER_TARGET_NOT_FOUND); } - - if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_EMAIL) { - throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL . ' ' . $targetDocument->getId()); - } } - - $message->setAttribute('targets', $targets); } $data = $message->getAttribute('data'); - if (!empty($subject)) { + if (!\is_null($targets)) { + $message->setAttribute('targets', $targets); + } + + if (!\is_null($subject)) { $data['subject'] = $subject; } - if (!empty($content)) { + if (!\is_null($content)) { $data['content'] = $content; } - if (!empty($html)) { + if (!\is_null($html)) { $data['html'] = $html; } + if (!\is_null($cc)) { + $data['cc'] = $cc; + } + + if (!\is_null($bcc)) { + $data['bcc'] = $bcc; + } + $message->setAttribute('data', $data); - if (!empty($description)) { + if (!\is_null($description)) { $message->setAttribute('description', $description); } - if (!empty($status)) { + if (!\is_null($status)) { $message->setAttribute('status', $status); } @@ -2731,19 +2822,19 @@ App::patch('/v1/messaging/messages/sms/:messageId') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_MESSAGE) ->param('messageId', '', new UID(), 'Message ID.') - ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Topic IDs.', true) - ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true) - ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) - ->param('description', '', new Text(256), 'Description for Message.', true) - ->param('content', '', new Text(64230), 'Email Content.', true) - ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true) + ->param('description', null, new Text(256), 'Description for Message.', true) + ->param('content', null, new Text(64230), 'Email Content.', true) + ->param('status', null, new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $description, string $content, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->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()) { @@ -2767,16 +2858,20 @@ App::patch('/v1/messaging/messages/sms/:messageId') } if (!\is_null($targets)) { - foreach ($targets as $target) { - $targetDocument = $dbForProject->getDocument('targets', $target); + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $targets), + Query::equal('providerType', [MESSAGE_TYPE_SMS]), + Query::limit(\count($targets)), + ]); - if ($targetDocument->isEmpty()) { + if (\count($foundTargets) !== \count($targets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS); + } + + foreach ($foundTargets as $target) { + if ($target->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); @@ -2784,17 +2879,17 @@ App::patch('/v1/messaging/messages/sms/:messageId') $data = $message->getAttribute('data'); - if (!empty($content)) { + if (!\is_null($content)) { $data['content'] = $content; } $message->setAttribute('data', $data); - if (!empty($status)) { + if (!\is_null($status)) { $message->setAttribute('status', $status); } - if (!empty($description)) { + if (!\is_null($description)) { $message->setAttribute('description', $description); } @@ -2833,27 +2928,27 @@ App::patch('/v1/messaging/messages/push/:messageId') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_MESSAGE) ->param('messageId', '', new UID(), 'Message ID.') - ->param('topics', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Topic IDs.', true) - ->param('users', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of User IDs.', true) - ->param('targets', null, new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true) - ->param('description', '', new Text(256), 'Description for Message.', true) - ->param('title', '', new Text(256), 'Title for push notification.', true) - ->param('body', '', new Text(64230), 'Body for push notification.', true) + ->param('topics', null, new ArrayList(new UID()), 'List of Topic IDs.', true) + ->param('users', null, new ArrayList(new UID()), 'List of User IDs.', true) + ->param('targets', null, new ArrayList(new UID()), 'List of Targets IDs.', true) + ->param('description', null, new Text(256), 'Description for Message.', true) + ->param('title', null, new Text(256), 'Title for push notification.', true) + ->param('body', null, new Text(64230), 'Body for push notification.', true) ->param('data', null, new JSON(), 'Additional Data for push notification.', true) - ->param('action', '', new Text(256), 'Action for push notification.', true) - ->param('icon', '', new Text(256), 'Icon for push notification. Available only for Android and Web Platform.', true) - ->param('sound', '', new Text(256), 'Sound for push notification. Available only for Android and IOS Platform.', true) - ->param('color', '', new Text(256), 'Color for push notification. Available only for Android Platform.', true) - ->param('tag', '', new Text(256), 'Tag for push notification. Available only for Android Platform.', true) - ->param('badge', '', new Text(256), 'Badge for push notification. Available only for IOS Platform.', true) - ->param('status', '', new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('action', null, new Text(256), 'Action for push notification.', true) + ->param('icon', null, new Text(256), 'Icon for push notification. Available only for Android and Web platforms.', true) + ->param('sound', null, new Text(256), 'Sound for push notification. Available only for Android and iOS platforms.', true) + ->param('color', null, new Text(256), 'Color for push notification. Available only for Android platforms.', true) + ->param('tag', null, new Text(256), 'Tag for push notification. Available only for Android platforms.', true) + ->param('badge', null, new Integer(), 'Badge for push notification. Available only for iOS platforms.', true) + ->param('status', null, new WhiteList(['draft', 'cancelled', 'processing']), 'Message Status. Value must be either draft, cancelled, or processing.', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, string $description, string $title, string $body, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + ->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, ?int $badge, ?string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { $message = $dbForProject->getDocument('messages', $messageId); if ($message->isEmpty()) { @@ -2877,16 +2972,20 @@ App::patch('/v1/messaging/messages/push/:messageId') } if (!\is_null($targets)) { - foreach ($targets as $target) { - $targetDocument = $dbForProject->getDocument('targets', $target); + $foundTargets = $dbForProject->find('targets', [ + Query::equal('$id', $targets), + Query::equal('providerType', [MESSAGE_TYPE_PUSH]), + Query::limit(\count($targets)), + ]); - if ($targetDocument->isEmpty()) { + if (\count($foundTargets) !== \count($targets)) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH); + } + + foreach ($foundTargets as $target) { + if ($target->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); @@ -2894,53 +2993,53 @@ App::patch('/v1/messaging/messages/push/:messageId') $pushData = $message->getAttribute('data'); - if ($title) { + if (!\is_null($title)) { $pushData['title'] = $title; } - if ($body) { + if (!\is_null($body)) { $pushData['body'] = $body; } - if (!is_null($data)) { + if (!\is_null($data)) { $pushData['data'] = $data; } - if ($action) { + if (!\is_null($action)) { $pushData['action'] = $action; } - if ($icon) { + if (!\is_null($icon)) { $pushData['icon'] = $icon; } - if ($sound) { + if (!\is_null($sound)) { $pushData['sound'] = $sound; } - if ($color) { + if (!\is_null($color)) { $pushData['color'] = $color; } - if ($tag) { + if (!\is_null($tag)) { $pushData['tag'] = $tag; } - if ($badge) { + if (!\is_null($badge)) { $pushData['badge'] = $badge; } $message->setAttribute('data', $pushData); - if (!empty($status)) { + if (!\is_null($status)) { $message->setAttribute('status', $status); } - if (!empty($description)) { + if (!\is_null($description)) { $message->setAttribute('description', $description); } - if (!is_null($scheduledAt)) { + if (!\is_null($scheduledAt)) { $message->setAttribute('scheduledAt', $scheduledAt); } diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 8a71f33d8b..5e2a2957e5 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1598,7 +1598,7 @@ App::delete('/v1/users/:userId/targets/:targetId') }); App::delete('/v1/users/identities/:identityId') - ->desc('Delete Identity') + ->desc('Delete identity') ->groups(['api', 'users']) ->label('event', 'users.[userId].identities.[identityId].delete') ->label('scope', 'users.write') @@ -1614,7 +1614,8 @@ App::delete('/v1/users/identities/:identityId') ->param('identityId', '', new UID(), 'Identity ID.') ->inject('response') ->inject('dbForProject') - ->action(function (string $identityId, Response $response, Database $dbForProject) { + ->inject('queueForEvents') + ->action(function (string $identityId, Response $response, Database $dbForProject, Event $queueForEvents) { $identity = $dbForProject->getDocument('identities', $identityId); @@ -1624,6 +1625,11 @@ App::delete('/v1/users/identities/:identityId') $dbForProject->deleteDocument('identities', $identityId); + $queueForEvents + ->setParam('userId', $identity->getAttribute('userId')) + ->setParam('identityId', $identity->getId()) + ->setPayload($response->output($identity, Response::MODEL_IDENTITY)); + return $response->noContent(); }); diff --git a/app/worker.php b/app/worker.php index a8e5607965..c9f1a0df1d 100644 --- a/app/worker.php +++ b/app/worker.php @@ -277,7 +277,7 @@ $worker $worker->workerStart() ->action(function () use ($workerName) { - Console::info("Worker $workerName started"); + Console::info("Worker $workerName started"); }); $worker->start(); diff --git a/composer.json b/composer.json index 70e822029e..f575959c81 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.3.*", - "utopia-php/messaging": "0.2.*", + "utopia-php/messaging": "0.8.*", "utopia-php/migration": "0.3.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.5.*", diff --git a/composer.lock b/composer.lock index ad6332a163..a5c5ec026c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "92b527993518d6b893e3d226064edac0", + "content-hash": "359b1e3bd27ac7362c6f8d145e64ae36", "packages": [ { "name": "adhocore/jwt", @@ -2272,26 +2272,28 @@ }, { "name": "utopia-php/messaging", - "version": "0.2.0", + "version": "0.8.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43" + "reference": "64eca3faf02a79831f219d4f3ae05cd278a88b4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/2d0f474a106bb1da285f85e105c29b46085d3a43", - "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/64eca3faf02a79831f219d4f3ae05cd278a88b4b", + "reference": "64eca3faf02a79831f219d4f3ae05cd278a88b4b", "shasum": "" }, "require": { "ext-curl": "*", + "ext-openssl": "*", "php": ">=8.0.0" }, "require-dev": { - "laravel/pint": "^1.2", + "laravel/pint": "1.13.*", "phpmailer/phpmailer": "6.8.*", - "phpunit/phpunit": "9.6.*" + "phpstan/phpstan": "1.10.*", + "phpunit/phpunit": "9.6.10" }, "type": "library", "autoload": { @@ -2314,9 +2316,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.2.0" + "source": "https://github.com/utopia-php/messaging/tree/0.8.0" }, - "time": "2023-09-14T20:48:42+00:00" + "time": "2023-12-15T06:44:08+00:00" }, { "name": "utopia-php/migration", diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index eb419ade11..c6107c6ba8 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -8,6 +8,7 @@ use Appwrite\Docker\Env; use Appwrite\Utopia\View; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Validator\Boolean; use Utopia\Validator\Text; use Utopia\Platform\Action; @@ -24,15 +25,16 @@ class Install extends Action { $this ->desc('Install Appwrite') - ->param('httpPort', '', new Text(4), 'Server HTTP port', true) - ->param('httpsPort', '', new Text(4), 'Server HTTPS port', true) + ->param('http-port', '', new Text(4), 'Server HTTP port', true) + ->param('https-port', '', new Text(4), 'Server HTTPS port', true) ->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true) ->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true) ->param('interactive', 'Y', new Text(1), 'Run an interactive session', true) - ->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive)); + ->param('no-start', false, new Boolean(true), 'Run an interactive session', true) + ->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive, $noStart) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart)); } - public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive): void + public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive, bool $noStart): void { $config = Config::getParam('variables'); $defaultHTTPPort = '80'; @@ -220,9 +222,11 @@ class Install extends Action } } - Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\""); - - $exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr); + $exit = 0; + if (!$noStart) { + Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\""); + $exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr); + } if ($exit !== 0) { $message = 'Failed to install Appwrite dockers'; diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php index e3f0458394..341ce42fc4 100644 --- a/src/Appwrite/Platform/Tasks/Upgrade.php +++ b/src/Appwrite/Platform/Tasks/Upgrade.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Tasks; use Utopia\CLI\Console; +use Utopia\Validator\Boolean; use Utopia\Validator\Text; class Upgrade extends Install @@ -16,15 +17,16 @@ class Upgrade extends Install { $this ->desc('Upgrade Appwrite') - ->param('httpPort', '', new Text(4), 'Server HTTP port', true) - ->param('httpsPort', '', new Text(4), 'Server HTTPS port', true) + ->param('http-port', '', new Text(4), 'Server HTTP port', true) + ->param('https-port', '', new Text(4), 'Server HTTPS port', true) ->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true) ->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true) ->param('interactive', 'Y', new Text(1), 'Run an interactive session', true) - ->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive)); + ->param('no-start', false, new Boolean(true), 'Run an interactive session', true) + ->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive, $noStart) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart)); } - public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive): void + public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive, bool $noStart): void { // Check for previous installation $data = @file_get_contents($this->path . '/docker-compose.yml'); @@ -37,6 +39,6 @@ class Upgrade extends Install Console::log(' └── docker-compose.yml'); Console::exit(1); } - parent::action($httpPort, $httpsPort, $organization, $image, $interactive); + parent::action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart); } } diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 8f945e947f..ebf657ecc5 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -13,22 +13,23 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Messaging\Adapters\SMS as SMSAdapter; -use Utopia\Messaging\Adapters\SMS\Mock; -use Utopia\Messaging\Adapters\SMS\Msg91; -use Utopia\Messaging\Adapters\SMS\Telesign; -use Utopia\Messaging\Adapters\SMS\Textmagic; -use Utopia\Messaging\Adapters\SMS\Twilio; -use Utopia\Messaging\Adapters\SMS\Vonage; -use Utopia\Messaging\Adapters\Push as PushAdapter; -use Utopia\Messaging\Adapters\Push\APNS; -use Utopia\Messaging\Adapters\Push\FCM; -use Utopia\Messaging\Adapters\Email as EmailAdapter; -use Utopia\Messaging\Adapters\Email\Mailgun; -use Utopia\Messaging\Adapters\Email\SendGrid; +use Utopia\Messaging\Adapter\Email as EmailAdapter; +use Utopia\Messaging\Adapter\Email\Mailgun; +use Utopia\Messaging\Adapter\Email\Sendgrid; +use Utopia\Messaging\Adapter\Push as PushAdapter; +use Utopia\Messaging\Adapter\Push\APNS; +use Utopia\Messaging\Adapter\Push\FCM; +use Utopia\Messaging\Adapter\SMS as SMSAdapter; +use Utopia\Messaging\Adapter\SMS\Mock; +use Utopia\Messaging\Adapter\SMS\Msg91; +use Utopia\Messaging\Adapter\SMS\Telesign; +use Utopia\Messaging\Adapter\SMS\Textmagic; +use Utopia\Messaging\Adapter\SMS\Twilio; +use Utopia\Messaging\Adapter\SMS\Vonage; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Push; use Utopia\Messaging\Messages\SMS; +use Utopia\Messaging\Response; use function Swoole\Coroutine\batch; @@ -62,7 +63,7 @@ class Messaging extends Action $payload = $message->getPayload() ?? []; if (empty($payload)) { - Console::error('Payload arg not found'); + Console::error('Payload not found.'); return; } @@ -84,14 +85,14 @@ class Messaging extends Action $usersId = $message->getAttribute('users', []); /** - * @var Document[] $recipients - */ + * @var Document[] $recipients + */ $recipients = []; if (\count($topicsId) > 0) { $topics = $dbForProject->find('topics', [Query::equal('$id', $topicsId)]); foreach ($topics as $topic) { - $targets = \array_filter($topic->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType')); + $targets = \array_filter($topic->getAttribute('targets'), fn(Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType')); $recipients = \array_merge($recipients, $targets); } } @@ -99,7 +100,7 @@ class Messaging extends Action if (\count($usersId) > 0) { $users = $dbForProject->find('users', [Query::equal('$id', $usersId)]); foreach ($users as $user) { - $targets = \array_filter($user->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType')); + $targets = \array_filter($user->getAttribute('targets'), fn(Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType')); $recipients = \array_merge($recipients, $targets); } } @@ -115,14 +116,16 @@ class Messaging extends Action ]); /** - * @var array> $identifiersByProviderId - */ + * @var array> $identifiersByProviderId + */ $identifiersByProviderId = []; /** - * @var Document[] $providers - */ - $providers = []; + * @var Document[] $providers + */ + $providers = [ + $primaryProvider->getId() => $primaryProvider + ]; foreach ($recipients as $recipient) { $providerId = $recipient->getAttribute('providerId'); @@ -139,29 +142,28 @@ class Messaging extends Action } /** - * @var array[] $results - */ + * @var array[] $results + */ $results = batch(\array_map(function ($providerId) use ($identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) { return function () use ($providerId, $identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) { - $provider = new Document(); - - if ($primaryProvider->getId() === $providerId) { - $provider = $primaryProvider; + if (\array_key_exists($providerId, $providers)) { + $provider = $providers[$providerId]; } else { $provider = $dbForProject->getDocument('providers', $providerId, [Query::equal('enabled', [true])]); if ($provider->isEmpty()) { $provider = $primaryProvider; + } else { + $providers[$providerId] = $provider; } } - $providers[] = $provider; $identifiers = $identifiersByProviderId[$providerId]; $adapter = match ($provider->getAttribute('type')) { MESSAGE_TYPE_SMS => $this->sms($provider), MESSAGE_TYPE_PUSH => $this->push($provider), - MESSAGE_TYPE_EMAIL => $this->email($provider), + MESSAGE_TYPE_EMAIL => $this->email($provider), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; @@ -169,8 +171,8 @@ class Messaging extends Action $batches = \array_chunk($identifiers, $maxBatchSize); $batchIndex = 0; - $results = batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) { - return function () use ($batch, $message, $provider, $adapter, $batchIndex) { + return batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex, $dbForProject) { + return function () use ($batch, $message, $provider, $adapter, $batchIndex, $dbForProject) { $deliveredTotal = 0; $deliveryErrors = []; $messageData = clone $message; @@ -179,13 +181,29 @@ class Messaging extends Action $data = match ($provider->getAttribute('type')) { MESSAGE_TYPE_SMS => $this->buildSMSMessage($messageData, $provider), MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData), - MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($messageData, $provider), + MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($dbForProject, $messageData, $provider), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; try { - $adapter->send($data); - $deliveredTotal += \count($batch); + $response = new Response($provider->getAttribute('type')); + $response->fromArray($adapter->send($data)); + + $deliveredTotal += $response->getDeliveredTo(); + $details[] = $response->getDetails(); + foreach ($details as $detail) { + if ($detail['status'] === 'failure') { + $deliveryErrors[] = "Failed sending to target {$detail['recipient']} with error: {$detail['error']}"; + } + + // Deleting push targets when token has expired. + if ($detail['error'] === 'Expired device token.') { + $target = $dbForProject->findOne('targets', [Query::equal('identifier', [$detail['recipient']])]); + if ($target instanceof Document && !$target->isEmpty()) { + $dbForProject->deleteDocument('targets', $target->getId()); + } + } + } } catch (\Exception $e) { $deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage(); } finally { @@ -197,8 +215,6 @@ class Messaging extends Action } }; }, $batches)); - - return $results; }; }, \array_keys($identifiersByProviderId))); @@ -313,7 +329,7 @@ class Messaging extends Action 'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']), 'textmagic' => new Textmagic($credentials['username'], $credentials['apiKey']), 'telesign' => new Telesign($credentials['username'], $credentials['password']), - 'msg91' => new Msg91($credentials['senderId'], $credentials['authKey']), + 'msg91' => new Msg91($credentials['senderId'], $credentials['authKey'], $credentials['templateId']), 'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']), default => null }; @@ -323,6 +339,7 @@ class Messaging extends Action { $credentials = $provider->getAttribute('credentials'); return match ($provider->getAttribute('provider')) { + 'mock' => new Mock('username', 'password'), 'apns' => new APNS( $credentials['authKey'], $credentials['authKeyId'], @@ -330,7 +347,7 @@ class Messaging extends Action $credentials['bundleId'], $credentials['endpoint'] ), - 'fcm' => new FCM($credentials['serverKey']), + 'fcm' => new FCM($credentials['serviceAccountJSON']), default => null }; } @@ -339,21 +356,51 @@ class Messaging extends Action { $credentials = $provider->getAttribute('credentials'); return match ($provider->getAttribute('provider')) { + 'mock' => new Mock('username', 'password'), 'mailgun' => new Mailgun($credentials['apiKey'], $credentials['domain'], $credentials['isEuRegion']), - 'sendgrid' => new SendGrid($credentials['apiKey']), + 'sendgrid' => new Sendgrid($credentials['apiKey']), default => null }; } - private function buildEmailMessage(Document $message, Document $provider): Email + private function buildEmailMessage(Database $dbForProject, Document $message, Document $provider): Email { - $from = $provider['options']['from']; - $to = $message['to']; - $subject = $message['data']['subject']; - $content = $message['data']['content']; - $html = $message['data']['html']; + $fromName = $provider['options']['fromName']; + $fromEmail = $provider['options']['fromEmail']; + $replyToEmail = null; + $replyToName = null; - return new Email($to, $subject, $content, $from, null, $html); + if (isset($provider['options']['replyToName']) && isset($provider['options']['replyToEmail'])) { + $replyToName = $provider['options']['replyToName']; + $replyToEmail = $provider['options']['replyToEmail']; + } + + $data = $message['data'] ?? []; + $ccTargets = $data['cc'] ?? []; + $bccTargets = $data['bcc'] ?? []; + $cc = []; + $bcc = []; + + if (\count($ccTargets) > 0) { + $ccTargets = $dbForProject->find('targets', [Query::equal('identifier', $ccTargets)]); + foreach ($ccTargets as $ccTarget) { + $cc[] = ['email' => $ccTarget['identifier']]; + } + } + + if (\count($bccTargets) > 0) { + $bccTargets = $dbForProject->find('targets', [Query::equal('identifier', $bccTargets)]); + foreach ($bccTargets as $bccTarget) { + $bcc[] = ['email' => $bccTarget['identifier']]; + } + } + + $to = $message['to']; + $subject = $data['subject']; + $content = $data['content']; + $html = $data['html']; + + return new Email($to, $subject, $content, $fromName, $fromEmail, $replyToName, $replyToEmail, $cc, $bcc, null, $html); } private function buildSMSMessage(Document $message, Document $provider): SMS diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php b/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php index 120b3edc3f..27c818d319 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php @@ -7,6 +7,7 @@ class Topics extends Base public const ALLOWED_ATTRIBUTES = [ 'name', 'description', + 'total' ]; /** diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index c7388069a1..8e73747047 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -72,6 +72,7 @@ class Executor ) { $runtimeId = "$projectId-$deploymentId-build"; $route = "/runtimes"; + $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); $params = [ 'runtimeId' => $runtimeId, 'source' => $source, @@ -84,10 +85,9 @@ class Executor 'cpus' => $this->cpus, 'memory' => $this->memory, 'version' => $version, + 'timeout' => $timeout, ]; - $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); - $response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout); $status = $response['headers']['status-code']; @@ -111,7 +111,7 @@ class Executor string $projectId, callable $callback ) { - $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); + $timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900); $runtimeId = "$projectId-$deploymentId-build"; $route = "/runtimes/{$runtimeId}/logs"; diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 2854d0bf42..2c630e3f75 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -1790,8 +1790,8 @@ trait Base } }'; case self::$CREATE_MAILGUN_PROVIDER: - return 'mutation createMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $from: String!, $isEuRegion: Boolean!) { - messagingCreateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, from: $from, isEuRegion: $isEuRegion) { + return 'mutation createMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $fromName: String!, $fromEmail: String!, $isEuRegion: Boolean!, $replyToName: String, $replyToEmail: String) { + messagingCreateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, fromName: $fromName, fromEmail: $fromEmail, isEuRegion: $isEuRegion, replyToName: $replyToName, replyToEmail: $replyToEmail) { _id name provider @@ -1800,8 +1800,8 @@ trait Base } }'; case self::$CREATE_SENDGRID_PROVIDER: - return 'mutation createSendgridProvider($providerId: String!, $name: String!, $from: String!, $apiKey: String!) { - messagingCreateSendgridProvider(providerId: $providerId, name: $name, from: $from, apiKey: $apiKey) { + return 'mutation createSendgridProvider($providerId: String!, $name: String!, $fromName: String!, $fromEmail: String!, $apiKey: String!, $replyToName: String, $replyToEmail: String) { + messagingCreateSendgridProvider(providerId: $providerId, name: $name, fromName: $fromName, fromEmail: $fromEmail, apiKey: $apiKey, replyToName: $replyToName, replyToEmail: $replyToEmail) { _id name provider @@ -1860,8 +1860,8 @@ trait Base } }'; case self::$CREATE_FCM_PROVIDER: - return 'mutation createFcmProvider($providerId: String!, $name: String!, $serverKey: String!) { - messagingCreateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) { + return 'mutation createFcmProvider($providerId: String!, $name: String!, $serviceAccountJSON: Json) { + messagingCreateFcmProvider(providerId: $providerId, name: $name, serviceAccountJSON: $serviceAccountJSON) { _id name provider @@ -1904,8 +1904,8 @@ trait Base } }'; case self::$UPDATE_MAILGUN_PROVIDER: - return 'mutation updateMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $isEuRegion: Boolean, $enabled: Boolean) { - messagingUpdateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, isEuRegion: $isEuRegion, enabled: $enabled) { + return 'mutation updateMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $isEuRegion: Boolean, $enabled: Boolean, $fromName: String, $fromEmail: String) { + messagingUpdateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, isEuRegion: $isEuRegion, enabled: $enabled, fromName: $fromName, fromEmail: $fromEmail) { _id name provider @@ -1914,8 +1914,8 @@ trait Base } }'; case self::$UPDATE_SENDGRID_PROVIDER: - return 'mutation messagingUpdateSendgridProvider($providerId: String!, $name: String!, $apiKey: String!) { - messagingUpdateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey) { + return 'mutation messagingUpdateSendgridProvider($providerId: String!, $name: String!, $apiKey: String!, $enabled: Boolean, $fromName: String, $fromEmail: String) { + messagingUpdateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey, enabled: $enabled, fromName: $fromName, fromEmail: $fromEmail) { _id name provider @@ -1974,8 +1974,8 @@ trait Base } }'; case self::$UPDATE_FCM_PROVIDER: - return 'mutation updateFcmProvider($providerId: String!, $name: String!, $serverKey: String!) { - messagingUpdateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) { + return 'mutation updateFcmProvider($providerId: String!, $name: String!, $serviceAccountJSON: Json) { + messagingUpdateFcmProvider(providerId: $providerId, name: $name, serviceAccountJSON: $serviceAccountJSON) { _id name provider @@ -2098,8 +2098,8 @@ 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, $scheduledAt: String) { - messagingCreateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) { + return 'mutation createEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String!, $content: String!, $status: String, $description: String, $html: Boolean, $cc: [String], $bcc: [String], $scheduledAt: String) { + messagingCreateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, cc: $cc, bcc: $bcc, scheduledAt: $scheduledAt) { _id topics users @@ -2178,8 +2178,8 @@ 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, $scheduledAt: String) { - messagingUpdateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) { + return 'mutation updateEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String, $content: String, $status: String, $description: String, $html: Boolean, $cc: [String], $bcc: [String], $scheduledAt: String) { + messagingUpdateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, cc: $cc, bcc: $bcc, scheduledAt: $scheduledAt) { _id topics users diff --git a/tests/e2e/Services/GraphQL/MessagingTest.php b/tests/e2e/Services/GraphQL/MessagingTest.php index d1a084cfc1..1828411483 100644 --- a/tests/e2e/Services/GraphQL/MessagingTest.php +++ b/tests/e2e/Services/GraphQL/MessagingTest.php @@ -23,14 +23,16 @@ class MessagingTest extends Scope 'providerId' => ID::unique(), 'name' => 'Sengrid1', 'apiKey' => 'my-apikey', - 'from' => 'sender-email@my-domain.com', + 'fromName' => 'Sender Name', + 'fromEmail' => 'sender-email@my-domain.com', ], 'Mailgun' => [ 'providerId' => ID::unique(), 'name' => 'Mailgun1', 'apiKey' => 'my-apikey', 'domain' => 'my-domain', - 'from' => 'sender-email@my-domain.com', + 'fromName' => 'Sender Name', + 'fromEmail' => 'sender-email@my-domain.com', 'isEuRegion' => false, ], 'Twilio' => [ @@ -71,7 +73,12 @@ class MessagingTest extends Scope 'Fcm' => [ 'providerId' => ID::unique(), 'name' => 'FCM1', - 'serverKey' => 'my-serverkey', + 'serviceAccountJSON' => [ + 'type' => 'service_account', + "project_id" => "test-project", + "private_key_id" => "test-private-key-id", + "private_key" => "test-private-key", + ] ], 'Apns' => [ 'providerId' => ID::unique(), @@ -97,6 +104,7 @@ class MessagingTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]), $graphQLPayload); + \array_push($providers, $response['body']['data']['messagingCreate' . $key . 'Provider']); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals($providersParams[$key]['name'], $response['body']['data']['messagingCreate' . $key . 'Provider']['name']); @@ -155,7 +163,12 @@ class MessagingTest extends Scope 'Fcm' => [ 'providerId' => $providers[7]['_id'], 'name' => 'FCM2', - 'serverKey' => 'my-serverkey', + 'serviceAccountJSON' => [ + 'type' => 'service_account', + 'project_id' => 'test-project', + 'private_key_id' => 'test-project-id', + 'private_key' => "test-private-key", + ] ], 'Apns' => [ 'providerId' => $providers[8]['_id'], @@ -374,7 +387,8 @@ class MessagingTest extends Scope 'providerId' => ID::unique(), 'name' => 'Sengrid1', 'apiKey' => 'my-apikey', - 'from' => 'sender-email@my-domain.com', + 'fromName' => 'Sender', + 'fromEmail' => 'sender-email@my-domain.com', ] ]; $query = $this->getQuery(self::$CREATE_SENDGRID_PROVIDER); @@ -543,7 +557,8 @@ class MessagingTest extends Scope $emailDSN = new DSN(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN')); $to = $emailDSN->getParam('to'); - $from = $emailDSN->getParam('from'); + $fromName = $emailDSN->getParam('fromName'); + $fromEmail = $emailDSN->getParam('fromEmail'); $isEuRegion = $emailDSN->getParam('isEuRegion'); $apiKey = $emailDSN->getPassword(); $domain = $emailDSN->getUser(); @@ -560,7 +575,8 @@ class MessagingTest extends Scope 'name' => 'Mailgun1', 'apiKey' => $apiKey, 'domain' => $domain, - 'from' => $from, + 'fromName' => $fromName, + 'fromEmail' => $fromEmail, 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN), ], ]; @@ -956,9 +972,9 @@ class MessagingTest extends Scope $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); $to = $pushDSN->getParam('to'); - $serverKey = $pushDSN->getPassword(); + $serviceAccountJSON = $pushDSN->getParam('serviceAccountJSON'); - if (empty($to) || empty($serverKey)) { + if (empty($to) || empty($serviceAccountJSON)) { $this->markTestSkipped('Push provider not configured'); } @@ -968,7 +984,12 @@ class MessagingTest extends Scope 'variables' => [ 'providerId' => ID::unique(), 'name' => 'FCM1', - 'serverKey' => $serverKey, + 'serviceAccountJSON' => [ + 'type' => 'service_account', + "project_id" => "test-project", + "private_key_id" => "test-private-key-id", + "private_key" => "test-private-key", + ] ], ]; $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ diff --git a/tests/e2e/Services/GraphQL/UsersTest.php b/tests/e2e/Services/GraphQL/UsersTest.php index 1dac9123a9..d243a45a4a 100644 --- a/tests/e2e/Services/GraphQL/UsersTest.php +++ b/tests/e2e/Services/GraphQL/UsersTest.php @@ -60,7 +60,8 @@ class UsersTest extends Scope 'name' => 'Mailgun1', 'apiKey' => 'api-key', 'domain' => 'domain', - 'from' => 'from@domain.com', + 'fromName' => 'sender name', + 'fromEmail' => 'from@domain.com', 'isEuRegion' => false, ], ]; diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index 690e503e77..bb09e0a247 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -5,6 +5,7 @@ namespace Tests\E2E\Services\Messaging; use Tests\E2E\Client; use Utopia\App; use Utopia\Database\Helpers\ID; +use Utopia\Database\Query; use Utopia\DSN\DSN; trait MessagingBase @@ -23,7 +24,8 @@ trait MessagingBase 'name' => 'Mailgun1', 'apiKey' => 'my-apikey', 'domain' => 'my-domain', - 'from' => 'sender-email@my-domain.com', + 'fromName' => 'sender name', + 'fromEmail' => 'sender-email@my-domain.com', 'isEuRegion' => false, ], 'twilio' => [ @@ -64,7 +66,12 @@ trait MessagingBase 'fcm' => [ 'providerId' => ID::unique(), 'name' => 'FCM1', - 'serverKey' => 'my-serverkey', + 'serviceAccountJSON' => [ + 'type' => 'service_account', + "project_id" => "test-project", + "private_key_id" => "test-private-key-id", + "private_key" => "test-private-key", + ], ], 'apns' => [ 'providerId' => ID::unique(), @@ -84,6 +91,7 @@ trait MessagingBase 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ]), $providersParams[$key]); + $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals($providersParams[$key]['name'], $response['body']['name']); \array_push($providers, $response['body']); @@ -134,7 +142,12 @@ trait MessagingBase ], 'fcm' => [ 'name' => 'FCM2', - 'serverKey' => 'my-serverkey', + 'serviceAccountJSON' => [ + 'type' => 'service_account', + "project_id" => "test-project", + "private_key_id" => "test-private-key-id", + "private_key" => "test-private-key", + ] ], 'apns' => [ 'name' => 'APNS2', @@ -161,11 +174,11 @@ trait MessagingBase 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ - 'name' => 'Mailgun2', - 'apiKey' => 'my-apikey', - 'domain' => 'my-domain', - 'isEuRegion' => true, - 'enabled' => false, + 'name' => 'Mailgun2', + 'apiKey' => 'my-apikey', + 'domain' => 'my-domain', + 'isEuRegion' => true, + 'enabled' => false, ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('Mailgun2', $response['body']['name']); @@ -229,7 +242,6 @@ trait MessagingBase ], [ 'topicId' => ID::unique(), 'name' => 'my-app', - 'description' => 'web app' ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals('my-app', $response['body']['name']); @@ -272,6 +284,32 @@ trait MessagingBase $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(1, \count($response['body']['topics'])); + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => [ + 'equal("total", [0])' + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, \count($response['body']['topics'])); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => [ + 'greaterThan("total", 0)' + ], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(0, \count($response['body']['topics'])); + return $topicId; } @@ -552,17 +590,16 @@ trait MessagingBase $emailDSN = new DSN(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN')); $to = $emailDSN->getParam('to'); - $from = $emailDSN->getParam('from'); - $isEuRegion = $emailDSN->getParam('isEuRegion'); + $fromName = $emailDSN->getParam('fromName'); + $fromEmail = $emailDSN->getParam('fromEmail'); $apiKey = $emailDSN->getPassword(); - $domain = $emailDSN->getUser(); - if (empty($to) || empty($from) || empty($apiKey) || empty($domain) || empty($isEuRegion)) { + if (empty($to) || empty($apiKey)) { $this->markTestSkipped('Email provider not configured'); } // Create provider - $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/mailgun', \array_merge([ + $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', \array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'], @@ -570,9 +607,8 @@ trait MessagingBase 'providerId' => ID::unique(), 'name' => 'Mailgun-provider', 'apiKey' => $apiKey, - 'domain' => $domain, - 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN), - 'from' => $from + 'fromName' => $fromName, + 'fromEmail' => $fromEmail ]); $this->assertEquals(201, $provider['headers']['status-code']); @@ -604,27 +640,13 @@ trait MessagingBase $this->assertEquals(201, $user['headers']['status-code']); - // Create Target - $target = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'], - ], [ - 'targetId' => ID::unique(), - 'providerType' => 'email', - 'providerId' => $provider['body']['$id'], - 'identifier' => $to, - ]); - - $this->assertEquals(201, $target['headers']['status-code']); - // Create Subscriber $subscriber = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['body']['$id'] . '/subscribers', \array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'subscriberId' => ID::unique(), - 'targetId' => $target['body']['$id'], + 'targetId' => $user['body']['targets'][0]['$id'], ]); $this->assertEquals(201, $subscriber['headers']['status-code']); @@ -637,13 +659,13 @@ trait MessagingBase ], [ 'messageId' => ID::unique(), 'topics' => [$topic['body']['$id']], - 'subject' => 'Khali beats Undertaker', - 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + 'subject' => 'New blog post', + 'content' => 'Check out the new blog post at http://localhost', ]); $this->assertEquals(201, $email['headers']['status-code']); - \sleep(5); + \sleep(2); $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'], [ 'origin' => 'http://localhost', @@ -662,7 +684,7 @@ trait MessagingBase /** * @depends testSendEmail */ - public function testUpdateEmail(array $email) + public function testUpdateEmail(array $email): void { $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], [ 'content-type' => 'application/json', @@ -721,8 +743,8 @@ trait MessagingBase $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN')); $to = $smsDSN->getParam('to'); $from = $smsDSN->getParam('from'); - $authKey = $smsDSN->getPassword(); $senderId = $smsDSN->getUser(); + $authKey = $smsDSN->getPassword(); if (empty($to) || empty($from) || empty($senderId) || empty($authKey)) { $this->markTestSkipped('SMS provider not configured'); @@ -735,7 +757,7 @@ trait MessagingBase 'x-appwrite-key' => $this->getProject()['apiKey'], ]), [ 'providerId' => ID::unique(), - 'name' => 'Msg91-1', + 'name' => 'Msg91Sender', 'senderId' => $senderId, 'authKey' => $authKey, 'from' => $from @@ -847,7 +869,7 @@ trait MessagingBase 'messageId' => ID::unique(), 'status' => 'draft', 'topics' => [$sms['body']['topics'][0]], - 'content' => '047487', + 'content' => 'Your OTP code is 123456', ]); $this->assertEquals(201, $sms['headers']['status-code']); @@ -862,7 +884,7 @@ trait MessagingBase $this->assertEquals(200, $sms['headers']['status-code']); - \sleep(5); + \sleep(2); $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $sms['body']['$id'], [ 'origin' => 'http://localhost', @@ -882,11 +904,11 @@ trait MessagingBase $this->markTestSkipped('Push DSN empty'); } - $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); - $to = $pushDSN->getParam('to'); - $serverKey = $pushDSN->getPassword(); + $dsn = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); + $to = $dsn->getParam('to'); + $serviceAccountJSON = $dsn->getParam('serviceAccountJSON'); - if (empty($to) || empty($serverKey)) { + if (empty($to) || empty($serviceAccountJSON)) { $this->markTestSkipped('Push provider not configured'); } @@ -898,7 +920,7 @@ trait MessagingBase ]), [ 'providerId' => ID::unique(), 'name' => 'FCM-1', - 'serverKey' => $serverKey, + 'serviceAccountJSON' => $serviceAccountJSON, ]); $this->assertEquals(201, $provider['headers']['status-code']); diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 3e63bd8740..4d844c7551 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -1661,7 +1661,6 @@ class ProjectsConsoleClientTest extends Scope foreach ($response['body'] as $key => $value) { if (\preg_match($pattern, $key)) { - \var_dump('Matched key: ' . $key); $matches[$key] = $value; } }