diff --git a/app/config/collections.php b/app/config/collections.php index 54e72740de..9435387207 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1903,7 +1903,29 @@ $commonCollections = [ 'filters' => [], ], [ - '$id' => ID::custom('total'), + '$id' => ID::custom('emailTotal'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('smsTotal'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('pushTotal'), 'type' => Database::VAR_INTEGER, 'format' => '', 'size' => 0, diff --git a/app/config/errors.php b/app/config/errors.php index 7932dcd3a9..40f6ad018f 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -872,7 +872,7 @@ return [ ], Exception::MESSAGE_MISSING_TARGET => [ 'name' => Exception::MESSAGE_MISSING_TARGET, - 'description' => 'Message with the requested ID is missing a target (Topics or Users or Targets).', + 'description' => 'Message with the requested ID has no recipients (topics or users or targets).', 'code' => 400, ], Exception::MESSAGE_ALREADY_SENT => [ @@ -920,4 +920,11 @@ return [ 'description' => 'Schedule with the requested ID could not be found.', 'code' => 404, ], + + /** Targets */ + Exception::TARGET_PROVIDER_INVALID_TYPE => [ + 'name' => Exception::TARGET_PROVIDER_INVALID_TYPE, + 'description' => 'Target has an invalid provider type.', + 'code' => 400, + ], ]; diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5bc6bbc222..dd009864a7 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -163,6 +163,11 @@ App::post('/v1/account') $user = Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); try { $target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([ + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), 'providerType' => MESSAGE_TYPE_EMAIL, @@ -707,7 +712,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $userDoc = Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); $dbForProject->createDocument('targets', new Document([ '$permissions' => [ - Permission::read(Role::any()), + Permission::read(Role::user($user->getId())), Permission::update(Role::user($user->getId())), Permission::delete(Role::user($user->getId())), ], @@ -1699,6 +1704,11 @@ App::post('/v1/account/tokens/phone') Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); try { $target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([ + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), 'providerType' => MESSAGE_TYPE_SMS, diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 0e865a7184..8e6c73f3bc 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -2260,7 +2260,19 @@ App::post('/v1/messaging/topics/:topicId/subscribers') try { $subscriber = $dbForProject->createDocument('subscribers', $subscriber); - Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('topics', $topicId, 'total', 1)); + + $totalAttribute = match ($target->getAttribute('providerType')) { + MESSAGE_TYPE_EMAIL => 'emailTotal', + MESSAGE_TYPE_SMS => 'smsTotal', + MESSAGE_TYPE_PUSH => 'pushTotal', + default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE), + }; + + Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute( + 'topics', + $topicId, + $totalAttribute, + )); } catch (DuplicateException) { throw new Exception(Exception::SUBSCRIBER_ALREADY_EXISTS); } @@ -2311,7 +2323,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers') throw new Exception(Exception::TOPIC_NOT_FOUND); } - \array_push($queries, Query::equal('topicInternalId', [$topic->getInternalId()])); + $queries[] = Query::equal('topicInternalId', [$topic->getInternalId()]); /** * Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries @@ -2512,8 +2524,23 @@ App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') throw new Exception(Exception::SUBSCRIBER_NOT_FOUND); } + $target = $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId')); + $dbForProject->deleteDocument('subscribers', $subscriberId); - Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('topics', $topicId, 'total', 1)); + + $totalAttribute = match ($target->getAttribute('providerType')) { + MESSAGE_TYPE_EMAIL => 'emailTotal', + MESSAGE_TYPE_SMS => 'smsTotal', + MESSAGE_TYPE_PUSH => 'pushTotal', + default => throw new Exception(Exception::TARGET_PROVIDER_INVALID_TYPE), + }; + + Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute( + 'topics', + $topicId, + $totalAttribute, + min: 0 + )); $queueForEvents ->setParam('topicId', $topic->getId()) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 942641a68a..9a9e37c32f 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -115,6 +115,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e if ($email) { try { $target = $dbForProject->createDocument('targets', new Document([ + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), 'providerType' => 'email', @@ -132,6 +137,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e if ($phone) { try { $target = $dbForProject->createDocument('targets', new Document([ + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), 'providerType' => 'sms', @@ -498,6 +508,11 @@ App::post('/v1/users/:userId/targets') try { $target = $dbForProject->createDocument('targets', new Document([ '$id' => $targetId, + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], 'providerId' => $providerId ?? null, 'providerInternalId' => $provider->getInternalId() ?? null, 'providerType' => $providerType, @@ -1227,6 +1242,11 @@ App::patch('/v1/users/:userId/email') } else { if (\strlen($email) !== 0) { $target = $dbForProject->createDocument('targets', new Document([ + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), 'providerType' => 'email', @@ -1305,6 +1325,11 @@ App::patch('/v1/users/:userId/phone') } else { if (\strlen($number) !== 0) { $target = $dbForProject->createDocument('targets', new Document([ + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], 'userId' => $user->getId(), 'userInternalId' => $user->getInternalId(), 'providerType' => 'sms', diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 98dee95c94..348fdacc0b 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -252,6 +252,7 @@ class Exception extends \Exception public const PROVIDER_NOT_FOUND = 'provider_not_found'; public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists'; public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type'; + public const PROVIDER_MISSING_CREDENTIALS = 'provider_missing_credentials'; /** Topic */ @@ -274,6 +275,9 @@ class Exception extends \Exception public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push'; public const MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule'; + /** Targets */ + public const TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type'; + /** Schedules */ public const SCHEDULE_NOT_FOUND = 'schedule_not_found'; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 7a7070b9e4..ad804d6d05 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Workers; use Appwrite\Auth\Auth; +use Appwrite\Extend\Exception; use Executor\Executor; use Throwable; use Utopia\Abuse\Abuse; @@ -263,12 +264,23 @@ class Deletes extends Action Query::equal('targetInternalId', [$target->getInternalId()]) ], $dbForProject, - function (Document $subscriber) use ($dbForProject) { + function (Document $subscriber) use ($dbForProject, $target) { $topicId = $subscriber->getAttribute('topicId'); $topicInternalId = $subscriber->getAttribute('topicInternalId'); $topic = $dbForProject->getDocument('topics', $topicId); if (!$topic->isEmpty() && $topic->getInternalId() === $topicInternalId) { - $dbForProject->decreaseDocumentAttribute('topics', $topicId, 'total', min: 0); + $totalAttribute = match ($target->getAttribute('providerType')) { + MESSAGE_TYPE_EMAIL => 'emailTotal', + MESSAGE_TYPE_SMS => 'smsTotal', + MESSAGE_TYPE_PUSH => 'pushTotal', + default => throw new Exception('Invalid target provider type'), + }; + $dbForProject->decreaseDocumentAttribute( + 'topics', + $topicId, + $totalAttribute, + min: 0 + ); } } ); diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 99730d475f..083eae4e0a 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -249,7 +249,7 @@ class Messaging extends Action } // Deleting push targets when token has expired. - if (($result['error'] ?? '') === 'Expired device token.') { + if (($result['error'] ?? '') === 'Expired device token') { $target = $dbForProject->findOne('targets', [ Query::equal('identifier', [$result['recipient']]) ]); diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php b/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php index 27c818d319..b73d93470f 100644 --- a/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Topics.php @@ -7,7 +7,9 @@ class Topics extends Base public const ALLOWED_ATTRIBUTES = [ 'name', 'description', - 'total' + 'emailTotal', + 'smsTotal', + 'pushTotal', ]; /** diff --git a/src/Appwrite/Utopia/Response/Model/Topic.php b/src/Appwrite/Utopia/Response/Model/Topic.php index 25889095fd..dd81430164 100644 --- a/src/Appwrite/Utopia/Response/Model/Topic.php +++ b/src/Appwrite/Utopia/Response/Model/Topic.php @@ -34,9 +34,21 @@ class Topic extends Model 'default' => '', 'example' => 'events', ]) - ->addRule('total', [ + ->addRule('emailTotal', [ 'type' => self::TYPE_INTEGER, - 'description' => 'Total count of subscribers subscribed to topic.', + 'description' => 'Total count of email subscribers subscribed to the topic.', + 'default' => 0, + 'example' => 100, + ]) + ->addRule('smsTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total count of SMS subscribers subscribed to the topic.', + 'default' => 0, + 'example' => 100, + ]) + ->addRule('pushTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total count of push subscribers subscribed to the topic.', 'default' => 0, 'example' => 100, ]) diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index a0e720de88..d4f290c3db 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -2026,6 +2026,9 @@ trait Base messagingCreateTopic(topicId: $topicId, name: $name) { _id name + emailTotal + smsTotal + pushTotal } }'; case self::$LIST_TOPICS: @@ -2035,6 +2038,9 @@ trait Base topics { _id name + emailTotal + smsTotal + pushTotal } } }'; @@ -2043,6 +2049,9 @@ trait Base messagingGetTopic(topicId: $topicId) { _id name + emailTotal + smsTotal + pushTotal } }'; case self::$UPDATE_TOPIC: @@ -2050,6 +2059,9 @@ trait Base messagingUpdateTopic(topicId: $topicId, name: $name) { _id name + emailTotal + smsTotal + pushTotal } }'; case self::$DELETE_TOPIC: diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index d93506e146..956ac5f68a 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -355,7 +355,9 @@ trait MessagingBase 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ 'queries' => [ - Query::equal('total', [0])->toString(), + Query::equal('emailTotal', [0])->toString(), + Query::equal('smsTotal', [0])->toString(), + Query::equal('pushTotal', [0])->toString(), ], ]); @@ -368,7 +370,9 @@ trait MessagingBase 'x-appwrite-key' => $this->getProject()['apiKey'], ], [ 'queries' => [ - Query::greaterThan('total', 0)->toString(), + Query::greaterThan('emailTotal', 0)->toString(), + Query::greaterThan('smsTotal', 0)->toString(), + Query::greaterThan('pushTotal', 0)->toString(), ], ]); @@ -390,7 +394,9 @@ trait MessagingBase ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('android-app', $response['body']['name']); - $this->assertEquals(0, $response['body']['total']); + $this->assertEquals(0, $response['body']['emailTotal']); + $this->assertEquals(0, $response['body']['smsTotal']); + $this->assertEquals(0, $response['body']['pushTotal']); } /** @@ -446,7 +452,9 @@ trait MessagingBase $this->assertEquals(200, $topic['headers']['status-code']); $this->assertEquals('android-app', $topic['body']['name']); - $this->assertEquals(1, $topic['body']['total']); + $this->assertEquals(1, $topic['body']['emailTotal']); + $this->assertEquals(0, $topic['body']['smsTotal']); + $this->assertEquals(0, $topic['body']['pushTotal']); $response2 = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topics['private']['$id'] . '/subscribers', \array_merge([ 'content-type' => 'application/json', @@ -695,7 +703,9 @@ trait MessagingBase $this->assertEquals(200, $topic['headers']['status-code']); $this->assertEquals('android-app', $topic['body']['name']); - $this->assertEquals(0, $topic['body']['total']); + $this->assertEquals(0, $topic['body']['emailTotal']); + $this->assertEquals(0, $topic['body']['smsTotal']); + $this->assertEquals(0, $topic['body']['pushTotal']); } /**