diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index c0596e65ec..2bc2096855 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1718,7 +1718,7 @@ App::post('/v1/account/jwt') App::post('/v1/account/targets/push') ->desc('Create Account\'s push target') ->groups(['api', 'account']) - ->label('error', __DIR__ . '/../../views/general/error.phtml') + ->label('scope', 'account') ->label('audits.event', 'target.create') ->label('audits.resource', 'target/response.$id') ->label('event', 'users.[userId].targets.[targetId].create') @@ -1728,10 +1728,9 @@ App::post('/v1/account/targets/push') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_TARGET) - ->label('docs', false) ->param('targetId', '', new CustomId(), 'Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') - ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.') ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)') + ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true) ->inject('queueForEvents') ->inject('user') ->inject('request') @@ -1742,10 +1741,6 @@ App::post('/v1/account/targets/push') $provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); - if ($provider->isEmpty()) { - throw new Exception(Exception::PROVIDER_NOT_FOUND); - } - if ($user->isEmpty()) { throw new Exception(Exception::USER_NOT_FOUND); } @@ -1768,8 +1763,8 @@ App::post('/v1/account/targets/push') Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), ], - 'providerId' => $providerId ?? null, - 'providerInternalId' => $provider->getInternalId() ?? null, + 'providerId' => !empty($providerId) ? $providerId : null, + 'providerInternalId' => !empty($providerId) ? $provider->getInternalId() : null, 'providerType' => MESSAGE_TYPE_PUSH, 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), @@ -3204,7 +3199,7 @@ App::put('/v1/account/verification/phone') App::put('/v1/account/targets/:targetId/push') ->desc('Update Account\'s push target') ->groups(['api', 'account']) - ->label('error', __DIR__ . '/../../views/general/error.phtml') + ->label('scope', 'account') ->label('audits.event', 'target.update') ->label('audits.resource', 'target/response.$id') ->label('event', 'users.[userId].targets.[targetId].update') diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 67cc338fca..51e046a6ec 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -14,6 +14,7 @@ use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Messages; use Appwrite\Utopia\Database\Validator\Queries\Providers; use Appwrite\Utopia\Database\Validator\Queries\Subscribers; +use Appwrite\Utopia\Database\Validator\Queries\Targets; use Appwrite\Utopia\Database\Validator\Queries\Topics; use Appwrite\Utopia\Response; use Utopia\App; @@ -2742,6 +2743,65 @@ App::get('/v1/messaging/messages/:messageId/logs') ]), Response::MODEL_LOG_LIST); }); +App::get('/v1/messaging/messages/:messageId/targets') + ->desc('List message targets') + ->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', 'listTargets') + ->label('sdk.description', '/docs/references/messaging/list-message-targets.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TARGET_LIST) + ->param('messageId', '', new UID(), 'Message ID.') + ->param('queries', [], new Targets(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Targets::ALLOWED_ATTRIBUTES), true) + ->inject('response') + ->inject('dbForProject') + ->inject('locale') + ->inject('geodb') + ->action(function (string $messageId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + $message = $dbForProject->getDocument('messages', $messageId); + + if ($message->isEmpty()) { + throw new Exception(Exception::MESSAGE_NOT_FOUND); + } + + $targetIDs = $message->getAttribute('targets'); + + if (empty($targetIDs)) { + $response->dynamic(new Document([ + 'targets' => [], + 'total' => 0, + ]), Response::MODEL_TARGET_LIST); + return; + } + + $queries = Query::parseQueries($queries); + + $queries[] = Query::equal('$id', $targetIDs); + + // Get cursor document if there was a cursor query + $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = reset($cursor); + + if ($cursor) { + $targetId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('targets', $targetId); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Target '{$targetId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $response->dynamic(new Document([ + 'targets' => $dbForProject->find('targets', $queries), + 'total' => $dbForProject->count('targets', $queries, APP_LIMIT_COUNT), + ]), Response::MODEL_TARGET_LIST); + }); + App::get('/v1/messaging/messages/:messageId') ->desc('Get a message') ->groups(['api', 'messaging']) diff --git a/docs/references/messaging/list-message-targets.md b/docs/references/messaging/list-message-targets.md new file mode 100644 index 0000000000..cc39250d5f --- /dev/null +++ b/docs/references/messaging/list-message-targets.md @@ -0,0 +1 @@ +List the targets associated with a message as set via the targets attribute. \ No newline at end of file diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 072e8d1299..e95d7835fd 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -433,14 +433,20 @@ class Messaging extends Action $bcc = []; if (\count($ccTargets) > 0) { - $ccTargets = $dbForProject->find('targets', [Query::equal('identifier', $ccTargets)]); + $ccTargets = $dbForProject->find('targets', [ + Query::equal('$id', $ccTargets), + Query::limit(\count($ccTargets)), + ]); foreach ($ccTargets as $ccTarget) { $cc[] = ['email' => $ccTarget['identifier']]; } } if (\count($bccTargets) > 0) { - $bccTargets = $dbForProject->find('targets', [Query::equal('identifier', $bccTargets)]); + $bccTargets = $dbForProject->find('targets', [ + Query::equal('$id', $bccTargets), + Query::limit(\count($bccTargets)), + ]); foreach ($bccTargets as $bccTarget) { $bcc[] = ['email' => $bccTarget['identifier']]; } diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index dafc618ac1..d6c4b59ce3 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -2,10 +2,10 @@ namespace Tests\E2E\Services\Messaging; +use Appwrite\Enum\MessageStatus; use Tests\E2E\Client; use Utopia\App; use Utopia\Database\Helpers\ID; -use Utopia\Database\Query; use Utopia\DSN\DSN; trait MessagingBase @@ -617,6 +617,47 @@ trait MessagingBase $this->assertEquals(204, $response['headers']['status-code']); } + public function testCreateDraftEmail() + { + // Create User + $response = $this->client->call(Client::METHOD_POST, '/users', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'userId' => ID::unique(), + 'email' => uniqid() . "@example.com", + 'password' => 'password', + 'name' => 'Messaging User', + ]); + + $this->assertEquals(201, $response['headers']['status-code'], "Error creating user: " . var_export($response['body'], true)); + + $user = $response['body']; + + $this->assertEquals(1, \count($user['targets'])); + $targetId = $user['targets'][0]['$id']; + + // Create Email + $response = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'targets' => [$targetId], + 'subject' => 'New blog post', + 'content' => 'Check out the new blog post at http://localhost', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + $message = $response['body']; + $this->assertEquals(MessageStatus::DRAFT, $message['status']); + + return $message; + } + public function testSendEmail() { if (empty(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'))) { @@ -1101,4 +1142,58 @@ trait MessagingBase $this->assertEquals(1, $message['body']['deliveredTotal']); $this->assertEquals(0, \count($message['body']['deliveryErrors'])); } + + /** + * @depends testCreateDraftEmail + */ + public function testListTargets(array $message) + { + $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/does_not_exist/targets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(404, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $message['$id'] . '/targets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $targetList = $response['body']; + $this->assertEquals(1, $targetList['total']); + $this->assertEquals(1, count($targetList['targets'])); + $this->assertEquals($message['targets'][0], $targetList['targets'][0]['$id']); + + // Test for empty targets + $response = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'subject' => 'New blog post', + 'content' => 'Check out the new blog post at http://localhost', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + $message = $response['body']; + + $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $message['$id'] . '/targets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $targetList = $response['body']; + $this->assertEquals(0, $targetList['total']); + $this->assertEquals(0, count($targetList['targets'])); + } }