diff --git a/app/config/collections.php b/app/config/collections.php index 1a56b160c6..bf2dd07975 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1600,6 +1600,17 @@ $commonCollections = [ 'array' => false, 'filters' => ['datetime'], ], + [ + '$id' => ID::custom('deliveredAt'), + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], [ '$id' => ID::custom('deliveryErrors'), 'type' => Database::VAR_STRING, @@ -1941,6 +1952,13 @@ $commonCollections = [ 'attributes' => ['identifier'], 'lengths' => [], 'orders' => [], + ], + [ + '$id' => ID::custom('_key_identifier_providerId'), + 'type' => Database::INDEX_UNIQUE, + 'attributes' => ['providerId', 'identifier'], + 'lengths' => [], + 'orders' => [], ] ], ], diff --git a/app/config/events.php b/app/config/events.php index 4aaa324e9c..b07d356470 100644 --- a/app/config/events.php +++ b/app/config/events.php @@ -258,6 +258,9 @@ return [ 'create' => [ '$description' => 'This event triggers when a message is created.', ], + 'update' => [ + '$description' => 'This event triggers when a message is updated.', + ], 'topics' => [ '$model' => Response::MODEL_TOPIC, '$resource' => true, diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 72ece29eaa..e4a008165b 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -589,7 +589,7 @@ App::get('/v1/messaging/providers') throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found."); } - $cursor->setValue($cursorDocument[0]); + $cursor->setValue($cursorDocument); } $filterQueries = Query::groupByType($queries)['filters']; @@ -623,7 +623,7 @@ App::get('/v1/messaging/providers/:providerId') $response->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/mailgun/:id') +App::patch('/v1/messaging/providers/mailgun/:providerId') ->desc('Update Mailgun Provider') ->groups(['api', 'messaging']) ->label('audits.event', 'providers.update') @@ -636,7 +636,7 @@ App::patch('/v1/messaging/providers/mailgun/:id') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROVIDER) - ->param('id', '', new UID(), 'Provider ID.') + ->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) @@ -645,8 +645,8 @@ App::patch('/v1/messaging/providers/mailgun/:id') ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $id, string $name, ?bool $enabled, ?bool $isEuRegion, string $from, string $apiKey, string $domain, Database $dbForProject, Response $response) { - $provider = $dbForProject->getDocument('providers', $id); + ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $from, string $apiKey, string $domain, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { throw new Exception(Exception::PROVIDER_NOT_FOUND); @@ -694,7 +694,7 @@ App::patch('/v1/messaging/providers/mailgun/:id') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/sendgrid/:id') +App::patch('/v1/messaging/providers/sendgrid/:providerId') ->desc('Update Sendgrid Provider') ->groups(['api', 'messaging']) ->label('audits.event', 'providers.update') @@ -707,14 +707,14 @@ App::patch('/v1/messaging/providers/sendgrid/:id') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROVIDER) - ->param('id', '', new UID(), 'Provider ID.') + ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $id, string $name, ?bool $enabled, string $apiKey, Database $dbForProject, Response $response) { - $provider = $dbForProject->getDocument('providers', $id); + ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { throw new Exception(Exception::PROVIDER_NOT_FOUND); @@ -746,7 +746,7 @@ App::patch('/v1/messaging/providers/sendgrid/:id') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/msg91/:id') +App::patch('/v1/messaging/providers/msg91/:providerId') ->desc('Update Msg91 Provider') ->groups(['api', 'messaging']) ->label('audits.event', 'providers.update') @@ -759,15 +759,15 @@ App::patch('/v1/messaging/providers/msg91/:id') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROVIDER) - ->param('id', '', new UID(), 'Provider ID.') + ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('senderId', '', new Text(0), 'Msg91 Sender ID.', true) ->param('authKey', '', new Text(0), 'Msg91 Auth Key.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $id, string $name, ?bool $enabled, string $senderId, string $authKey, Database $dbForProject, Response $response) { - $provider = $dbForProject->getDocument('providers', $id); + ->action(function (string $providerId, string $name, ?bool $enabled, string $senderId, string $authKey, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { throw new Exception(Exception::PROVIDER_NOT_FOUND); @@ -805,7 +805,7 @@ App::patch('/v1/messaging/providers/msg91/:id') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/telesign/:id') +App::patch('/v1/messaging/providers/telesign/:providerId') ->desc('Update Telesign Provider') ->groups(['api', 'messaging']) ->label('audits.event', 'providers.update') @@ -818,15 +818,15 @@ App::patch('/v1/messaging/providers/telesign/:id') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROVIDER) - ->param('id', '', new UID(), 'Provider ID.') + ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('username', '', new Text(0), 'Telesign username.', true) ->param('password', '', new Text(0), 'Telesign password.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $id, string $name, ?bool $enabled, string $username, string $password, Database $dbForProject, Response $response) { - $provider = $dbForProject->getDocument('providers', $id); + ->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $password, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { throw new Exception(Exception::PROVIDER_NOT_FOUND); @@ -864,7 +864,7 @@ App::patch('/v1/messaging/providers/telesign/:id') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/textmagic/:id') +App::patch('/v1/messaging/providers/textmagic/:providerId') ->desc('Update Textmagic Provider') ->groups(['api', 'messaging']) ->label('audits.event', 'providers.update') @@ -877,15 +877,15 @@ App::patch('/v1/messaging/providers/textmagic/:id') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROVIDER) - ->param('id', '', new UID(), 'Provider ID.') + ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('username', '', new Text(0), 'Textmagic username.', true) ->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $id, string $name, ?bool $enabled, string $username, string $apiKey, Database $dbForProject, Response $response) { - $provider = $dbForProject->getDocument('providers', $id); + ->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $apiKey, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { throw new Exception(Exception::PROVIDER_NOT_FOUND); @@ -923,7 +923,7 @@ App::patch('/v1/messaging/providers/textmagic/:id') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/twilio/:id') +App::patch('/v1/messaging/providers/twilio/:providerId') ->desc('Update Twilio Provider') ->groups(['api', 'messaging']) ->label('audits.event', 'providers.update') @@ -936,15 +936,15 @@ App::patch('/v1/messaging/providers/twilio/:id') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROVIDER) - ->param('id', '', new UID(), 'Provider ID.') + ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('accountSid', null, new Text(0), 'Twilio account secret ID.', true) ->param('authToken', null, new Text(0), 'Twilio authentication token.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $id, string $name, ?bool $enabled, string $accountSid, string $authToken, Database $dbForProject, Response $response) { - $provider = $dbForProject->getDocument('providers', $id); + ->action(function (string $providerId, string $name, ?bool $enabled, string $accountSid, string $authToken, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { throw new Exception(Exception::PROVIDER_NOT_FOUND); @@ -982,7 +982,7 @@ App::patch('/v1/messaging/providers/twilio/:id') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/vonage/:id') +App::patch('/v1/messaging/providers/vonage/:providerId') ->desc('Update Vonage Provider') ->groups(['api', 'messaging']) ->label('audits.event', 'providers.update') @@ -995,15 +995,15 @@ App::patch('/v1/messaging/providers/vonage/:id') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROVIDER) - ->param('id', '', new UID(), 'Provider ID.') + ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('apiKey', '', new Text(0), 'Vonage API key.', true) ->param('apiSecret', '', new Text(0), 'Vonage API secret.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $id, string $name, ?bool $enabled, string $apiKey, string $apiSecret, Database $dbForProject, Response $response) { - $provider = $dbForProject->getDocument('providers', $id); + ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $apiSecret, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { throw new Exception(Exception::PROVIDER_NOT_FOUND); @@ -1041,7 +1041,7 @@ App::patch('/v1/messaging/providers/vonage/:id') ->dynamic($provider, Response::MODEL_PROVIDER); }); -App::patch('/v1/messaging/providers/fcm/:id') +App::patch('/v1/messaging/providers/fcm/:providerId') ->desc('Update FCM Provider') ->groups(['api', 'messaging']) ->label('audits.event', 'providers.update') @@ -1054,14 +1054,14 @@ App::patch('/v1/messaging/providers/fcm/:id') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROVIDER) - ->param('id', '', new UID(), 'Provider ID.') + ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('serverKey', '', new Text(0), 'FCM Server Key.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $id, string $name, ?bool $enabled, string $serverKey, Database $dbForProject, Response $response) { - $provider = $dbForProject->getDocument('providers', $id); + ->action(function (string $providerId, string $name, ?bool $enabled, string $serverKey, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { throw new Exception(Exception::PROVIDER_NOT_FOUND); @@ -1092,7 +1092,7 @@ App::patch('/v1/messaging/providers/fcm/:id') }); -App::patch('/v1/messaging/providers/apns/:id') +App::patch('/v1/messaging/providers/apns/:providerId') ->desc('Update APNS Provider') ->groups(['api', 'messaging']) ->label('audits.event', 'providers.update') @@ -1105,7 +1105,7 @@ App::patch('/v1/messaging/providers/apns/:id') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROVIDER) - ->param('id', '', new UID(), 'Provider ID.') + ->param('providerId', '', new UID(), 'Provider ID.') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('authKey', '', new Text(0), 'APNS authentication key.', true) @@ -1115,8 +1115,8 @@ App::patch('/v1/messaging/providers/apns/:id') ->param('endpoint', '', new Text(0), 'APNS endpoint.', true) ->inject('dbForProject') ->inject('response') - ->action(function (string $id, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, Database $dbForProject, Response $response) { - $provider = $dbForProject->getDocument('providers', $id); + ->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { throw new Exception(Exception::PROVIDER_NOT_FOUND); @@ -1213,12 +1213,13 @@ App::post('/v1/messaging/messages/email') ->param('subject', '', new Text(998), 'Email Subject.') ->param('description', '', new Text(256), 'Description for Message.', true) ->param('content', '', new Text(64230), 'Email Content.') + ->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) ->inject('dbForProject') ->inject('project') ->inject('messaging') ->inject('response') - ->action(function (string $messageId, string $providerId, array $to, string $subject, string $description, string $content, string $html, Database $dbForProject, Document $project, Messaging $messaging, Response $response) { + ->action(function (string $messageId, string $providerId, array $to, string $subject, string $description, string $content, string $status, bool $html, Database $dbForProject, Document $project, Messaging $messaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; $provider = $dbForProject->getDocument('providers', $providerId); @@ -1238,20 +1239,63 @@ App::post('/v1/messaging/messages/email') 'html' => $html, 'description' => $description, ], - 'status' => 'processing', + 'status' => $status, 'search' => $messageId . ' ' . $description . ' ' . $subject, ])); - $messaging - ->setMessageId($message->getId()) - ->setProject($project) - ->trigger(); + if ($status === 'processing') { + $messaging + ->setMessageId($message->getId()) + ->setProject($project) + ->trigger(); + } $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($message, Response::MODEL_MESSAGE); }); +App::get('/v1/messaging/messages') + ->desc('List Messages') + ->groups(['api', 'messaging']) + ->label('scope', 'messages.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'listMessages') + ->label('sdk.description', '/docs/references/messaging/list-messages.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MESSAGE_LIST) + ->param('queries', [], new Providers(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Providers::ALLOWED_ATTRIBUTES), true) + ->inject('dbForProject') + ->inject('response') + ->action(function (array $queries, Database $dbForProject, Response $response) { + $queries = Query::parseQueries($queries); + + // Get cursor document if there was a cursor query + $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = reset($cursor); + + if ($cursor) { + $messageId = $cursor->getValue(); + $cursorDocument = Authorization::skip(fn () => $dbForProject->findOne('messages', [ + Query::equal('$id', [$messageId]), + ])); + + if ($cursorDocument === false || $cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $filterQueries = Query::groupByType($queries)['filters']; + $response->dynamic(new Document([ + 'total' => $dbForProject->count('messages', $filterQueries, APP_LIMIT_COUNT), + 'messages' => $dbForProject->find('messages', $queries), + ]), Response::MODEL_MESSAGE_LIST); + }); + App::get('/v1/messaging/messages/:messageId') ->desc('Get Message') ->groups(['api', 'messaging']) @@ -1275,3 +1319,77 @@ App::get('/v1/messaging/messages/:messageId') $response->dynamic($message, Response::MODEL_MESSAGE); }); + +App::post('/v1/messaging/messages/email/:messageId') + ->desc('Update an email.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'messages.update') + ->label('audits.resource', 'messages/{response.$id}') + ->label('scope', 'messages.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateEmail') + ->label('sdk.description', '/docs/references/messaging/update-email.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MESSAGE) + ->param('messageId', '', new UID(), 'Message ID.') + ->param('to', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of Topic IDs or List of User IDs or List of Target IDs.', true) + ->param('subject', '', new Text(998), 'Email Subject.', true) + ->param('description', '', new Text(256), 'Description for Message.', true) + ->param('content', '', new Text(64230), 'Email Content.', true) + ->param('status', '', new WhiteList(['draft', 'processing']), 'Message Status.', true) + ->param('html', false, new Boolean(), 'Is content of type HTML', true) + ->inject('dbForProject') + ->inject('project') + ->inject('messaging') + ->inject('response') + ->action(function (string $messageId, array $to, string $subject, string $description, string $content, string $status, bool $html, Database $dbForProject, Document $project, Messaging $messaging, Response $response) { + $message = $dbForProject->getDocument('messages', $messageId); + + if ($message->isEmpty()) { + throw new Exception(Exception::MESSAGE_NOT_FOUND); + } + + if (\count($to) > 0) { + $message->setAttribute('to', $to); + } + + $data = $message->getAttribute('data'); + + if (!empty($subject)) { + $data['subject'] = $subject; + } + + if (!empty($content)) { + $data['content'] = $content; + } + + if (!empty($description)) { + $data['description'] = $description; + } + + if (!empty($html)) { + $data['html'] = $html; + } + + $message->setAttribute('data', $data); + $message->setAttribute('search', $message->getId() . ' ' . $data['description'] . ' ' . $data['subject']); + + if (!empty($status)) { + $message->setAttribute('status', $status); + } + + $message = $dbForProject->updateDocument('messages', $message->getId(), $message); + + if ($status === 'processing') { + $messaging + ->setMessageId($message->getId()) + ->setProject($project) + ->trigger(); + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($message, Response::MODEL_MESSAGE); + }); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index e4ab995e19..9551a05ed9 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -419,14 +419,18 @@ App::post('/v1/users/:userId/targets') throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); } - $target = $dbForProject->createDocument('targets', new Document([ - '$id' => $targetId, - 'providerId' => $providerId, - 'providerInternalId' => $provider->getInternalId(), - 'userId' => $userId, - 'userInternalId' => $user->getInternalId(), - 'identifier' => $identifier, - ])); + try { + $target = $dbForProject->createDocument('targets', new Document([ + '$id' => $targetId, + 'providerId' => $providerId, + 'providerInternalId' => $provider->getInternalId(), + 'userId' => $userId, + 'userInternalId' => $user->getInternalId(), + 'identifier' => $identifier, + ])); + } catch (Duplicate) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } $dbForProject->deleteCachedDocument('users', $user->getId()); $response @@ -1229,6 +1233,10 @@ App::patch('/v1/users/:userId/targets/:targetId/identifier') throw new Exception(Exception::USER_TARGET_NOT_FOUND); } + if ($user->getId() !== $target->getAttribute('userId')) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + $target->setAttribute('identifier', $identifier); $target = $dbForProject->updateDocument('targets', $target->getId(), $target); @@ -1404,6 +1412,10 @@ App::delete('/v1/users/:userId/targets/:targetId') throw new Exception(Exception::USER_TARGET_NOT_FOUND); } + if ($user->getId() !== $target->getAttribute('userId')) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + $target = $dbForProject->deleteDocument('targets', $target->getId()); $dbForProject->deleteCachedDocument('users', $user->getId()); diff --git a/app/workers/messaging.php b/app/workers/messaging.php index d867f98101..14a08e8cfa 100644 --- a/app/workers/messaging.php +++ b/app/workers/messaging.php @@ -62,16 +62,16 @@ class MessagingV1 extends Worker $this->processMessage($message, $provider); } - private function processMessage(Document $messageRecord, Document $providerRecord): void + private function processMessage(Document $message, Document $provider): void { - $provider = match ($providerRecord->getAttribute('type')) { - 'sms' => $this->sms($providerRecord), - 'push' => $this->push($providerRecord), - 'email' => $this->email($providerRecord), + $adapter = match ($provider->getAttribute('type')) { + 'sms' => $this->sms($provider), + 'push' => $this->push($provider), + 'email' => $this->email($provider), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; - $recipientsId = $messageRecord->getAttribute('to'); + $recipientsId = $message->getAttribute('to'); /** * @var Document[] $recipients @@ -90,29 +90,31 @@ class MessagingV1 extends Worker $targets = $this->dbForProject->find('targets', [Query::equal('$id', $recipientsId)]); $recipients = \array_merge($recipients, $targets); - $recipients = \array_filter($recipients, fn (Document $recipient) => $recipient->getAttribute('providerId') === $providerRecord->getId()); + $recipients = \array_filter($recipients, function (Document $recipient) use ($provider) { + return $recipient->getAttribute('providerId') === $provider->getId(); + }); $identifiers = \array_map(function (Document $recipient) { return $recipient->getAttribute('identifier'); }, $recipients); - $maxBatchSize = $provider->getMaxMessagesPerRequest(); + $maxBatchSize = $adapter->getMaxMessagesPerRequest(); $batches = \array_chunk($identifiers, $maxBatchSize); - $results = batch(\array_map(function ($batch) use ($messageRecord, $providerRecord, $provider) { - return function () use ($batch, $messageRecord, $providerRecord, $provider) { + $results = batch(\array_map(function ($batch) use ($message, $provider, $adapter) { + return function () use ($batch, $message, $provider, $adapter) { $deliveredTo = 0; $deliveryErrors = []; - $messageData = clone $messageRecord; + $messageData = clone $message; $messageData->setAttribute('to', $batch); - $message = match ($providerRecord->getAttribute('type')) { - 'sms' => $this->buildSMSMessage($messageData, $providerRecord), + $data = match ($provider->getAttribute('type')) { + 'sms' => $this->buildSMSMessage($messageData, $provider), 'push' => $this->buildPushMessage($messageData), - 'email' => $this->buildEmailMessage($messageData, $providerRecord), + 'email' => $this->buildEmailMessage($messageData, $provider), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; try { - $provider->send($message); + $adapter->send($data); $deliveredTo += \count($batch); } catch (\Exception $e) { foreach ($batch as $identifier) { @@ -133,28 +135,28 @@ class MessagingV1 extends Worker $deliveredTo += $result['deliveredTo']; $deliveryErrors = \array_merge($deliveryErrors, $result['deliveryErrors']); } - $messageRecord->setAttribute('deliveryErrors', $deliveryErrors); + $message->setAttribute('deliveryErrors', $deliveryErrors); - if (\count($messageRecord->getAttribute('deliveryErrors')) > 0) { - $messageRecord->setAttribute('status', 'failed'); + if (\count($message->getAttribute('deliveryErrors')) > 0) { + $message->setAttribute('status', 'failed'); } else { - $messageRecord->setAttribute('status', 'sent'); + $message->setAttribute('status', 'sent'); } - $messageRecord->setAttribute('to', $recipientsId); - $messageRecord->setAttribute('deliveredTo', $deliveredTo); - $messageRecord->setAttribute('deliveryTime', DateTime::now()); + $message->setAttribute('to', $recipientsId); + $message->setAttribute('deliveredTo', $deliveredTo); + $message->setAttribute('deliveredAt', DateTime::now()); - $this->dbForProject->updateDocument('messages', $messageRecord->getId(), $messageRecord); + $this->dbForProject->updateDocument('messages', $message->getId(), $message); } public function shutdown(): void { } - private function sms(Document $document): ?SMSAdapter + private function sms(Document $provider): ?SMSAdapter { - $credentials = $document->getAttribute('credentials'); - return match ($document->getAttribute('provider')) { + $credentials = $provider->getAttribute('credentials'); + return match ($provider->getAttribute('provider')) { 'mock' => new Mock('username', 'password'), 'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']), 'text-magic' => new TextMagic($credentials['username'], $credentials['apiKey']), @@ -165,10 +167,10 @@ class MessagingV1 extends Worker }; } - private function push(Document $document): ?PushAdapter + private function push(Document $provider): ?PushAdapter { - $credentials = $document->getAttribute('credentials'); - return match ($document->getAttribute('provider')) { + $credentials = $provider->getAttribute('credentials'); + return match ($provider->getAttribute('provider')) { 'apns' => new APNS( $credentials['authKey'], $credentials['authKeyId'], @@ -181,10 +183,10 @@ class MessagingV1 extends Worker }; } - private function email(Document $document): ?EmailAdapter + private function email(Document $provider): ?EmailAdapter { - $credentials = $document->getAttribute('credentials'); - return match ($document->getAttribute('provider')) { + $credentials = $provider->getAttribute('credentials'); + return match ($provider->getAttribute('provider')) { 'mailgun' => new Mailgun($credentials['apiKey'], $credentials['domain'], $credentials['isEuRegion']), 'sendgrid' => new SendGrid($credentials['apiKey']), default => null diff --git a/src/Appwrite/Utopia/Response/Model/Message.php b/src/Appwrite/Utopia/Response/Model/Message.php index 2f75997048..5e4a358490 100644 --- a/src/Appwrite/Utopia/Response/Model/Message.php +++ b/src/Appwrite/Utopia/Response/Model/Message.php @@ -32,11 +32,18 @@ class Message extends Any ]) ->addRule('deliveryTime', [ 'type' => self::TYPE_DATETIME, - 'description' => 'Time the message is delivered at.', + 'description' => 'The scheduled time for message.', 'required' => false, 'default' => DateTime::now(), 'example' => self::TYPE_DATETIME_EXAMPLE, ]) + ->addRule('deliveredAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'The time when the message was delivered.', + 'required' => false, + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) ->addRule('deliveryErrors', [ 'type' => self::TYPE_STRING, 'description' => 'Delivery errors if any.', diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 3a7859662a..0a93ac34c4 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -1823,8 +1823,8 @@ trait Base } }'; case self::$GET_PROVIDER: - return 'query getProvider($id: String!) { - messagingGetProvider(id: $id) { + return 'query getProvider($providerId: String!) { + messagingGetProvider(providerId: $providerId) { _id name provider @@ -1834,8 +1834,8 @@ trait Base } }'; case self::$UPDATE_MAILGUN_PROVIDER: - return 'mutation updateMailgunProvider($id: String!, $name: String!, $domain: String!, $apiKey: String!, $isEuRegion: Boolean, $enabled: Boolean) { - messagingUpdateMailgunProvider(id: $id, 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) { + messagingUpdateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, isEuRegion: $isEuRegion, enabled: $enabled) { _id name provider @@ -1845,8 +1845,8 @@ trait Base } }'; case self::$UPDATE_SENDGRID_PROVIDER: - return 'mutation messagingUpdateSendgridProvider($id: String!, $name: String!, $apiKey: String!) { - messagingUpdateSendgridProvider(id: $id, name: $name, apiKey: $apiKey) { + return 'mutation messagingUpdateSendgridProvider($providerId: String!, $name: String!, $apiKey: String!) { + messagingUpdateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey) { _id name provider @@ -1856,8 +1856,8 @@ trait Base } }'; case self::$UPDATE_TWILIO_PROVIDER: - return 'mutation updateTwilioProvider($id: String!, $name: String!, $accountSid: String!, $authToken: String!) { - messagingUpdateTwilioProvider(id: $id, name: $name, accountSid: $accountSid, authToken: $authToken) { + return 'mutation updateTwilioProvider($providerId: String!, $name: String!, $accountSid: String!, $authToken: String!) { + messagingUpdateTwilioProvider(providerId: $providerId, name: $name, accountSid: $accountSid, authToken: $authToken) { _id name provider @@ -1867,8 +1867,8 @@ trait Base } }'; case self::$UPDATE_TELESIGN_PROVIDER: - return 'mutation updateTelesignProvider($id: String!, $name: String!, $username: String!, $password: String!) { - messagingUpdateTelesignProvider(id: $id, name: $name, username: $username, password: $password) { + return 'mutation updateTelesignProvider($providerId: String!, $name: String!, $username: String!, $password: String!) { + messagingUpdateTelesignProvider(providerId: $providerId, name: $name, username: $username, password: $password) { _id name provider @@ -1878,8 +1878,8 @@ trait Base } }'; case self::$UPDATE_TEXTMAGIC_PROVIDER: - return 'mutation updateTextmagicProvider($id: String!, $name: String!, $username: String!, $apiKey: String!) { - messagingUpdateTextmagicProvider(id: $id, 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 @@ -1889,8 +1889,8 @@ trait Base } }'; case self::$UPDATE_MSG91_PROVIDER: - return 'mutation updateMsg91Provider($id: String!, $name: String!, $senderId: String!, $authKey: String!) { - messagingUpdateMsg91Provider(id: $id, name: $name, senderId: $senderId, authKey: $authKey) { + return 'mutation updateMsg91Provider($providerId: String!, $name: String!, $senderId: String!, $authKey: String!) { + messagingUpdateMsg91Provider(providerId: $providerId, name: $name, senderId: $senderId, authKey: $authKey) { _id name provider @@ -1900,8 +1900,8 @@ trait Base } }'; case self::$UPDATE_VONAGE_PROVIDER: - return 'mutation updateVonageProvider($id: String!, $name: String!, $apiKey: String!, $apiSecret: String!) { - messagingUpdateVonageProvider(id: $id, name: $name, apiKey: $apiKey, apiSecret: $apiSecret) { + return 'mutation updateVonageProvider($providerId: String!, $name: String!, $apiKey: String!, $apiSecret: String!) { + messagingUpdateVonageProvider(providerId: $providerId, name: $name, apiKey: $apiKey, apiSecret: $apiSecret) { _id name provider @@ -1911,8 +1911,8 @@ trait Base } }'; case self::$UPDATE_FCM_PROVIDER: - return 'mutation updateFcmProvider($id: String!, $name: String!, $serverKey: String!) { - messagingUpdateFcmProvider(id: $id, name: $name, serverKey: $serverKey) { + return 'mutation updateFcmProvider($providerId: String!, $name: String!, $serverKey: String!) { + messagingUpdateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) { _id name provider @@ -1922,8 +1922,8 @@ trait Base } }'; case self::$UPDATE_APNS_PROVIDER: - return 'mutation updateApnsProvider($id: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) { - messagingUpdateApnsProvider(id: $id, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) { + return 'mutation updateApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) { + messagingUpdateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) { _id name provider @@ -1933,8 +1933,8 @@ trait Base } }'; case self::$DELETE_PROVIDER: - return 'mutation deleteProvider($id: String!) { - messagingDeleteProvider(id: $id) { + return 'mutation deleteProvider($providerId: String!) { + messagingDeleteProvider(providerId: $providerId) { status } }'; diff --git a/tests/e2e/Services/GraphQL/MessagingTest.php b/tests/e2e/Services/GraphQL/MessagingTest.php index 1bc5e70477..99bad52887 100644 --- a/tests/e2e/Services/GraphQL/MessagingTest.php +++ b/tests/e2e/Services/GraphQL/MessagingTest.php @@ -104,53 +104,53 @@ class MessagingTest extends Scope { $providersParams = [ 'Sendgrid' => [ - 'id' => $providers[0]['_id'], + 'providerId' => $providers[0]['_id'], 'name' => 'Sengrid2', 'apiKey' => 'my-apikey', ], 'Mailgun' => [ - 'id' => $providers[1]['_id'], + 'providerId' => $providers[1]['_id'], 'name' => 'Mailgun2', 'apiKey' => 'my-apikey', 'domain' => 'my-domain', ], 'Twilio' => [ - 'id' => $providers[2]['_id'], + 'providerId' => $providers[2]['_id'], 'name' => 'Twilio2', 'accountSid' => 'my-accountSid', 'authToken' => 'my-authToken', ], 'Telesign' => [ - 'id' => $providers[3]['_id'], + 'providerId' => $providers[3]['_id'], 'name' => 'Telesign2', 'username' => 'my-username', 'password' => 'my-password', ], 'Textmagic' => [ - 'id' => $providers[4]['_id'], + 'providerId' => $providers[4]['_id'], 'name' => 'Textmagic2', 'username' => 'my-username', 'apiKey' => 'my-apikey', ], 'Msg91' => [ - 'id' => $providers[5]['_id'], + 'providerId' => $providers[5]['_id'], 'name' => 'Ms91-2', 'senderId' => 'my-senderid', 'authKey' => 'my-authkey', ], 'Vonage' => [ - 'id' => $providers[6]['_id'], + 'providerId' => $providers[6]['_id'], 'name' => 'Vonage2', 'apiKey' => 'my-apikey', 'apiSecret' => 'my-apisecret', ], 'Fcm' => [ - 'id' => $providers[7]['_id'], + 'providerId' => $providers[7]['_id'], 'name' => 'FCM2', 'serverKey' => 'my-serverkey', ], 'Apns' => [ - 'id' => $providers[8]['_id'], + 'providerId' => $providers[8]['_id'], 'name' => 'APNS2', 'authKey' => 'my-authkey', 'authKeyId' => 'my-authkeyid', @@ -182,7 +182,7 @@ class MessagingTest extends Scope ], [ 'query' => $this->getQuery('update_mailgun_provider'), 'variables' => [ - 'id' => $providers[1]['_id'], + 'providerId' => $providers[1]['_id'], 'name' => 'Mailgun2', 'apiKey' => 'my-apikey', 'domain' => 'my-domain', @@ -224,7 +224,7 @@ class MessagingTest extends Scope $graphQLPayload = [ 'query' => $query, 'variables' => [ - 'id' => $providers[0]['_id'], + 'providerId' => $providers[0]['_id'], ] ]; $response = $this->client->call(Client::METHOD_POST, '/graphql', [ @@ -246,7 +246,7 @@ class MessagingTest extends Scope $graphQLPayload = [ 'query' => $query, 'variables' => [ - 'id' => $provider['_id'], + 'providerId' => $provider['_id'], ] ]; $response = $this->client->call(Client::METHOD_POST, '/graphql', [