diff --git a/.env b/.env index ad551e705a..f77083a035 100644 --- a/.env +++ b/.env @@ -87,7 +87,7 @@ _APP_LOGGING_PROVIDER= _APP_LOGGING_CONFIG= _APP_GRAPHQL_MAX_BATCH_SIZE=10 _APP_GRAPHQL_MAX_COMPLEXITY=250 -_APP_GRAPHQL_MAX_DEPTH=3 +_APP_GRAPHQL_MAX_DEPTH=4 _APP_DOCKER_HUB_USERNAME= _APP_DOCKER_HUB_PASSWORD= _APP_VCS_GITHUB_APP_NAME= @@ -98,4 +98,7 @@ _APP_VCS_GITHUB_CLIENT_SECRET= _APP_VCS_GITHUB_WEBHOOK_SECRET= _APP_MIGRATIONS_FIREBASE_CLIENT_ID= _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET= -_APP_ASSISTANT_OPENAI_API_KEY= \ No newline at end of file +_APP_ASSISTANT_OPENAI_API_KEY= +_APP_MESSAGE_SMS_TEST_DSN= +_APP_MESSAGE_EMAIL_TEST_DSN= +_APP_MESSAGE_PUSH_TEST_DSN= diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 842d61ff1c..14e1ac5e44 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -99,6 +99,7 @@ jobs: Users, Webhooks, VCS, + Messaging, ] steps: diff --git a/.gitignore b/.gitignore index 3151de5adb..ac88830b49 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ debug/ app/sdks dev/yasd_init.php .phpunit.result.cache +Makefile diff --git a/CHANGES.md b/CHANGES.md index 83e09896c8..5634340d69 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -484,7 +484,7 @@ ## Features - Added Phone Authentication by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3357 - Added Twilio Support - - Added TextMagic Support + - Added Textmagic Support - Added Telesign Support - Added Endpoint to create Phone Session (`POST:/v1/account/sessions/phone`) - Added Endpoint to confirm Phone Session (`PUT:/v1/account/sessions/phone`) diff --git a/app/config/collections.php b/app/config/collections.php index db229ce87a..b3e3417555 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -5,7 +5,7 @@ use Utopia\Config\Config; use Utopia\Database\Database; use Utopia\Database\Helpers\ID; -$providers = Config::getParam('providers', []); +$providers = Config::getParam('oAuthProviders', []); $auth = Config::getParam('auth', []); /** @@ -221,6 +221,17 @@ $commonCollections = [ 'array' => false, 'filters' => ['subQueryMemberships'], ], + [ + '$id' => ID::custom('targets'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['subQueryTargets'], + ], [ '$id' => ID::custom('search'), 'type' => Database::VAR_STRING, @@ -1366,7 +1377,608 @@ $commonCollections = [ ], ], ], - ]; + + 'providers' => [ + '$collection' => ID::custom(DATABASE::METADATA), + '$id' => ID::custom('providers'), + 'name' => 'Providers', + 'attributes' => [ + [ + '$id' => ID::custom('name'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('provider'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('type'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('enabled'), + 'type' => Database::VAR_BOOLEAN, + 'signed' => true, + 'size' => 0, + 'format' => '', + 'filters' => [], + 'required' => true, + 'default' => true, + 'array' => false, + ], + [ + '$id' => ID::custom('credentials'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => ['json', 'encrypt'], + ], + [ + '$id' => ID::custom('options'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => false, + 'filters' => ['json'], + ], + [ + '$id' => ID::custom('search'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 65535, + 'signed' => true, + 'required' => false, + 'default' => '', + 'array' => false, + 'filters' => ['providerSearch'], + ], + ], + 'indexes' => [ + [ + '$id' => ID::custom('_key_provider'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['provider'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ], + [ + '$id' => ID::custom('_key_name'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['name'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ], + [ + '$id' => ID::custom('_key_type'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['type'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ], + [ + '$id' => ID::custom('_key_enabled_type'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['enabled','type'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ], + [ + '$id' => ID::custom('_key_search'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['search'], + 'lengths' => [], + 'orders' => [], + ] + ], + ], + + 'messages' => [ + '$collection' => ID::custom(DATABASE::METADATA), + '$id' => ID::custom('messages'), + 'name' => 'Messages', + 'attributes' => [ + [ + '$id' => ID::custom('providerType'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('description'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 256, + 'signed' => true, + 'required' => false, + 'default' => '', + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('status'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => 'processing', + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('data'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 65535, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => ['json'], + ], + [ + '$id' => ID::custom('topics'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 21845, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], + [ + '$id' => ID::custom('users'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 21845, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], + [ + '$id' => ID::custom('targets'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 21845, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], + [ + '$id' => ID::custom('scheduledAt'), + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + '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, + 'format' => '', + 'size' => 65535, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => true, + 'filters' => [], + ], + [ + '$id' => ID::custom('deliveredTotal'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('search'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => '', + 'array' => false, + 'filters' => ['messageSearch'], + ], + ], + 'indexes' => [ + [ + '$id' => ID::custom('_key_search'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['search'], + 'lengths' => [], + 'orders' => [], + ], + ], + ], + + 'topics' => [ + '$collection' => ID::custom(DATABASE::METADATA), + '$id' => ID::custom('topics'), + 'name' => 'Topics', + 'attributes' => [ + [ + '$id' => ID::custom('name'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('description'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 2048, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('total'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('targets'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['subQueryTopicTargets'], + ], + [ + '$id' => ID::custom('search'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => '', + 'array' => false, + 'filters' => ['topicSearch'], + ], + ], + 'indexes' => [ + [ + '$id' => ID::custom('_key_name'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['name'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_search'), + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['search'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ] + ], + ], + + 'subscribers' => [ + '$collection' => ID::custom(DATABASE::METADATA), + '$id' => ID::custom('subscribers'), + 'name' => 'Subscribers', + 'attributes' => [ + [ + '$id' => ID::custom('targetId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('targetInternalId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('userId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('userInternalId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('topicId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('topicInternalId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('providerType'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 128, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [ + [ + '$id' => ID::custom('_key_targetId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['targetId'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_targetInternalId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['targetInternalId'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_userId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['userId'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_userInternalId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['userInternalId'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_topicId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['topicId'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_topicInternalId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['topicInternalId'], + 'lengths' => [], + 'orders' => [], + ] + ], + ], + + 'targets' => [ + '$collection' => ID::custom(DATABASE::METADATA), + '$id' => ID::custom('targets'), + 'name' => 'Targets', + 'attributes' => [ + [ + '$id' => ID::custom('userId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('userInternalId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('providerType'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('providerId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('providerInternalId'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('identifier'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('name'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [ + [ + '$id' => ID::custom('_key_userId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['userId'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ], + [ + '$id' => ID::custom('_key_userInternalId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['userInternalId'], + 'lengths' => [], + 'orders' => [Database::ORDER_ASC], + ], + [ + '$id' => ID::custom('_key_providerId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['providerId'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_providerInternalId'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['providerInternalId'], + 'lengths' => [], + 'orders' => [], + ], + [ + '$id' => ID::custom('_key_identifier'), + 'type' => Database::INDEX_UNIQUE, + 'attributes' => ['identifier'], + 'lengths' => [], + 'orders' => [], + ], + ], + ], +]; $projectCollections = array_merge([ 'databases' => [ @@ -3412,7 +4024,7 @@ $consoleCollections = array_merge([ 'filters' => ['json'], ], [ - '$id' => ID::custom('authProviders'), + '$id' => ID::custom('oAuthProviders'), 'type' => Database::VAR_STRING, 'format' => '', 'size' => 16384, diff --git a/app/config/errors.php b/app/config/errors.php index c0628920d9..2e35bfb881 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -103,6 +103,16 @@ return [ 'description' => 'This method was not fully implemented yet. If you believe this is a mistake, please upgrade your Appwrite server version.', 'code' => 405, ], + Exception::GENERAL_INVALID_EMAIL => [ + 'name' => Exception::GENERAL_INVALID_EMAIL, + 'description' => 'Value must be a valid email address.', + 'code' => 400, + ], + Exception::GENERAL_INVALID_PHONE => [ + 'name' => Exception::GENERAL_INVALID_PHONE, + 'description' => 'Value must be a valid phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', + 'code' => 400, + ], /** User Errors */ Exception::USER_COUNT_EXCEEDED => [ @@ -251,6 +261,16 @@ return [ 'description' => 'User phone is already verified', 'code' => 409 ], + Exception::USER_TARGET_NOT_FOUND => [ + 'name' => Exception::USER_TARGET_NOT_FOUND, + 'description' => 'The target could not be found.', + 'code' => 404, + ], + Exception::USER_TARGET_ALREADY_EXISTS => [ + 'name' => Exception::USER_TARGET_ALREADY_EXISTS, + 'description' => 'A target with the same ID already exists.', + 'code' => 409, + ], /** Teams */ Exception::TEAM_NOT_FOUND => [ @@ -773,4 +793,83 @@ return [ 'code' => 503, 'publish' => false ], + + /** Providers */ + Exception::PROVIDER_NOT_FOUND => [ + 'name' => Exception::PROVIDER_NOT_FOUND, + 'description' => 'Provider with the requested ID could not be found.', + 'code' => 404, + ], + Exception::PROVIDER_ALREADY_EXISTS => [ + 'name' => Exception::PROVIDER_ALREADY_EXISTS, + 'description' => 'Provider with the requested ID already exists.', + 'code' => 409, + ], + Exception::PROVIDER_INCORRECT_TYPE => [ + 'name' => Exception::PROVIDER_INCORRECT_TYPE, + 'description' => 'Provider with the requested ID is of incorrect type: ', + 'code' => 400, + ], + + /** Topics */ + Exception::TOPIC_NOT_FOUND => [ + 'name' => Exception::TOPIC_NOT_FOUND, + 'description' => 'Topic with the request ID could not be found.', + 'code' => 404, + ], + Exception::TOPIC_ALREADY_EXISTS => [ + 'name' => Exception::TOPIC_ALREADY_EXISTS, + 'description' => 'Topic with the request ID already exists.', + 'code' => 409, + ], + + /** Subscribers */ + Exception::SUBSCRIBER_NOT_FOUND => [ + 'name' => Exception::SUBSCRIBER_NOT_FOUND, + 'description' => 'Subscriber with the request ID could not be found.', + 'code' => 404, + ], + Exception::SUBSCRIBER_ALREADY_EXISTS => [ + 'name' => Exception::SUBSCRIBER_ALREADY_EXISTS, + 'description' => 'Subscriber with the request ID already exists.', + 'code' => 409, + ], + + /** Messages */ + Exception::MESSAGE_NOT_FOUND => [ + 'name' => Exception::MESSAGE_NOT_FOUND, + 'description' => 'Message with the requested ID could not be found.', + 'code' => 404, + ], + Exception::MESSAGE_MISSING_TARGET => [ + 'name' => Exception::MESSAGE_MISSING_TARGET, + 'description' => 'Message with the requested ID is missing a target (Topics or Users or Targets).', + 'code' => 400, + ], + Exception::MESSAGE_ALREADY_SENT => [ + 'name' => Exception::MESSAGE_ALREADY_SENT, + 'description' => 'Message with the requested ID has already been sent.', + 'code' => 400, + ], + Exception::MESSAGE_ALREADY_SCHEDULED => [ + 'name' => Exception::MESSAGE_ALREADY_SCHEDULED, + 'description' => 'Message with the requested ID has already been scheduled for delivery.', + 'code' => 400, + ], + Exception::MESSAGE_TARGET_NOT_EMAIL => [ + 'name' => Exception::MESSAGE_TARGET_NOT_EMAIL, + 'description' => 'Message with the target ID is not an email target:', + 'code' => 400, + ], + Exception::MESSAGE_TARGET_NOT_SMS => [ + 'name' => Exception::MESSAGE_TARGET_NOT_SMS, + 'description' => 'Message with the target ID is not an SMS target:', + 'code' => 400, + ], + Exception::MESSAGE_TARGET_NOT_PUSH => [ + 'name' => Exception::MESSAGE_TARGET_NOT_PUSH, + 'description' => 'Message with the target ID is not a push target:', + 'code' => 400, + ], + ]; diff --git a/app/config/events.php b/app/config/events.php index c0b6cc3b41..b0db9090fb 100644 --- a/app/config/events.php +++ b/app/config/events.php @@ -44,6 +44,20 @@ return [ '$description' => 'This event triggers when a verification token for a user is validated.' ], ], + 'targets' => [ + '$model' => Response::MODEL_TARGET, + '$resource' => true, + '$description' => 'This event triggers on any user\'s target event.', + 'create' => [ + '$description' => 'This event triggers when a user\'s target is created.', + ], + 'update' => [ + '$description' => 'This event triggers when a user\'s target is updated.', + ], + 'delete' => [ + '$description' => 'This event triggers when a user\'s target is deleted.', + ], + ], 'create' => [ '$description' => 'This event triggers when a user is created.' ], @@ -237,6 +251,56 @@ return [ '$description' => 'This event triggers when a function is updated.', ] ], + 'messages' => [ + '$model' => Response::MODEL_MESSAGE, + '$resource' => true, + '$description' => 'This event triggers on any messaging event.', + '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, + '$description' => 'This event triggers on any topic event.', + 'create' => [ + '$description' => 'This event triggers when a topic is created.', + ], + 'update' => [ + '$description' => 'This event triggers when a topic is updated.', + ], + 'delete' => [ + '$description' => 'This event triggers when a topic is deleted.' + ], + 'subscribers' => [ + '$model' => Response::MODEL_SUBSCRIBER, + '$resource' => true, + '$description' => 'This event triggers on any subscriber event.', + 'create' => [ + '$description' => 'This event triggers when a subscriber is created.', + ], + 'delete' => [ + '$description' => 'This event triggers when a subscriber is deleted.' + ], + ], + ], + 'providers' => [ + '$model' => Response::MODEL_PROVIDER, + '$resource' => true, + '$description' => 'This event triggers on any provider event.', + 'create' => [ + '$description' => 'This event triggers when a provider is created.', + ], + 'update' => [ + '$description' => 'This event triggers when a provider is updated.', + ], + 'delete' => [ + '$description' => 'This event triggers when a provider is deleted.' + ], + ], 'rules' => [ '$model' => Response::MODEL_PROXY_RULE, '$resource' => true, diff --git a/app/config/roles.php b/app/config/roles.php index 65d5694bbf..944fcf3577 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -21,6 +21,10 @@ $member = [ 'avatars.read', 'execution.read', 'execution.write', + 'targets.read', + 'targets.write', + 'subscribers.write', + 'subscribers.read', 'assistant.read', ]; @@ -60,6 +64,16 @@ $admins = [ 'migrations.write', 'vcs.read', 'vcs.write', + 'targets.read', + 'targets.write', + 'providers.write', + 'providers.read', + 'messages.write', + 'messages.read', + 'topics.write', + 'topics.read', + 'subscribers.write', + 'subscribers.read' ]; return [ diff --git a/app/config/scopes.php b/app/config/scopes.php index acafc7623c..ddc1de21f3 100644 --- a/app/config/scopes.php +++ b/app/config/scopes.php @@ -76,6 +76,36 @@ return [ // List of publicly visible scopes 'health.read' => [ 'description' => 'Access to read your project\'s health status', ], + 'providers.read' => [ + 'description' => 'Access to read your project\'s providers', + ], + 'providers.write' => [ + 'description' => 'Access to create, update, and delete your project\'s providers', + ], + 'messages.read' => [ + 'description' => 'Access to read your project\'s messages', + ], + 'messages.write' => [ + 'description' => 'Access to create, update, and delete your project\'s messages', + ], + 'topics.read' => [ + 'description' => 'Access to read your project\'s topics', + ], + 'topics.write' => [ + 'description' => 'Access to create, update, and delete your project\'s topics', + ], + 'subscribers.read' => [ + 'description' => 'Access to read your project\'s subscribers', + ], + 'subscribers.write' => [ + 'description' => 'Access to create, update, and delete your project\'s subscribers', + ], + 'targets.read' => [ + 'description' => 'Access to read your project\'s targets', + ], + 'targets.write' => [ + 'description' => 'Access to create, update, and delete your project\'s targets', + ], 'rules.read' => [ 'description' => 'Access to read your project\'s proxy rules', ], diff --git a/app/config/services.php b/app/config/services.php index 5c2233dfc6..c4fb98c59a 100644 --- a/app/config/services.php +++ b/app/config/services.php @@ -251,4 +251,17 @@ return [ 'optional' => false, 'icon' => '/images/services/migrations.png', ], + 'messaging' => [ + 'key' => 'messaging', + 'name' => 'Messaging', + 'subtitle' => 'The Messaging service allows you to send messages to any provider type (SMTP, push notification, SMS, etc.).', + 'description' => '/docs/services/messaging.md', + 'controller' => 'api/messaging.php', + 'sdk' => true, + 'docs' => true, + 'docsUrl' => 'https://appwrite.io/docs/server/messaging', + 'tests' => true, + 'optional' => true, + 'icon' => '/images/services/messaging.png', + ] ]; diff --git a/app/config/variables.php b/app/config/variables.php index 9d555bf013..c9329f6d55 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -440,7 +440,7 @@ return [ 'variables' => [ [ 'name' => '_APP_SMS_PROVIDER', - 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, text-magic, telesign, msg91, and vonage.", + 'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'.\n\nEnsure `[USER]` and `[SECRET]` are URL encoded if they contain any non-alphanumeric characters.\n\nAvailable providers are twilio, Textmagic, telesign, msg91, and vonage.", 'introduction' => '0.15.0', 'default' => '', 'required' => false, diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 54967fb502..b85bdc0133 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -8,7 +8,6 @@ use Appwrite\Auth\Validator\Phone; use Appwrite\Detector\Detector; use Appwrite\Event\Event; use Appwrite\Event\Mail; -use Appwrite\Event\Phone as EventPhone; use Appwrite\Extend\Exception; use Appwrite\Network\Validator\Email; use Utopia\Validator\Host; @@ -45,6 +44,7 @@ use Utopia\Validator\WhiteList; use Appwrite\Auth\Validator\PasswordHistory; use Appwrite\Auth\Validator\PasswordDictionary; use Appwrite\Auth\Validator\PersonalData; +use Appwrite\Event\Messaging; $oauthDefaultSuccess = '/auth/oauth2/success'; $oauthDefaultFailure = '/auth/oauth2/failure'; @@ -69,7 +69,7 @@ App::post('/v1/account') ->label('abuse-limit', 10) ->param('userId', '', new CustomId(), 'Unique 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('email', '', new Email(), 'User email.') - ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary']) + ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary']) ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) ->inject('request') ->inject('response') @@ -148,7 +148,22 @@ App::post('/v1/account') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); + $user = Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); + try { + $target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([ + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => MESSAGE_TYPE_EMAIL, + 'identifier' => $email, + ]))); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]); + } catch (Duplicate) { + $existingTarget = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ]); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate) { throw new Exception(Exception::USER_ALREADY_EXISTS); } @@ -299,7 +314,7 @@ App::get('/v1/account/sessions/oauth2/:provider') ->label('sdk.methodType', 'webAuth') ->label('abuse-limit', 50) ->label('abuse-key', 'ip:{ip}') - ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('providers'), fn($node) => (!$node['mock'])))) . '.') + ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn($node) => (!$node['mock'])))) . '.') ->param('success', '', fn($clients) => new Host($clients), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients']) ->param('failure', '', fn($clients) => new Host($clients), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients']) ->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true) @@ -311,14 +326,14 @@ App::get('/v1/account/sessions/oauth2/:provider') $protocol = $request->getProtocol(); $callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId(); - $providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false; + $providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false; if (!$providerEnabled) { throw new Exception(Exception::PROJECT_PROVIDER_DISABLED, 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.'); } - $appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? ''; - $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}'; + $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? ''; + $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}'; if (!empty($appSecret) && isset($appSecret['version'])) { $key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']); @@ -358,7 +373,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->label('scope', 'public') ->label('docs', false) ->param('projectId', '', new Text(1024), 'Project ID.') - ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.') + ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.') ->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true) ->param('state', '', new Text(2048), 'Login state params.', true) ->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true) @@ -391,7 +406,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId') ->label('origin', '*') ->label('docs', false) ->param('projectId', '', new Text(1024), 'Project ID.') - ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.') + ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.') ->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true) ->param('state', '', new Text(2048), 'Login state params.', true) ->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true) @@ -430,7 +445,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ->label('docs', false) ->label('usage.metric', 'sessions.{scope}.requests.create') ->label('usage.params', ['provider:{request.provider}']) - ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.') + ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.') ->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true) ->param('state', '', new Text(2048), 'OAuth2 state params.', true) ->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true) @@ -448,9 +463,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId(); $defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => '']; $validateURL = new URL(); - $appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? ''; - $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}'; - $providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false; + $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? ''; + $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}'; + $providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false; $className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider); @@ -458,7 +473,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); } - $providers = Config::getParam('providers'); + $providers = Config::getParam('oAuthProviders'); $providerName = $providers[$provider]['name'] ?? ''; /** @var Appwrite\Auth\OAuth2 $oauth2 */ @@ -664,7 +679,18 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'accessedAt' => DateTime::now(), ]); $user->removeAttribute('$internalId'); - Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); + $userDoc = Authorization::skip(fn() => $dbForProject->createDocument('users', $user)); + $dbForProject->createDocument('targets', new Document([ + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], + 'userId' => $userDoc->getId(), + 'userInternalId' => $userDoc->getInternalId(), + 'providerType' => MESSAGE_TYPE_EMAIL, + 'identifier' => $email, + ])); } catch (Duplicate) { $failureRedirect(Exception::USER_ALREADY_EXISTS); } @@ -1243,8 +1269,7 @@ App::post('/v1/account/sessions/phone') ->inject('queueForEvents') ->inject('queueForMessaging') ->inject('locale') - ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, EventPhone $queueForMessaging, Locale $locale) { - + ->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale) { if (empty(App::getEnv('_APP_SMS_PROVIDER'))) { throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } @@ -1294,6 +1319,21 @@ App::post('/v1/account/sessions/phone') $user->removeAttribute('$internalId'); Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); + try { + $target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([ + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => MESSAGE_TYPE_SMS, + 'identifier' => $phone, + ]))); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]); + } catch (Duplicate) { + $existingTarget = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$phone]), + ]); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } $secret = Auth::codeGenerator(); @@ -1331,9 +1371,19 @@ App::post('/v1/account/sessions/phone') $message = $message->setParam('{{token}}', $secret); $message = $message->render(); + + $messageDoc = new Document([ + '$id' => $token->getId(), + 'data' => [ + 'content' => $message, + ], + ]); + $queueForMessaging - ->setRecipient($phone) - ->setMessage($message) + ->setMessage($messageDoc) + ->setRecipients([$phone]) + ->setProviderType(MESSAGE_TYPE_SMS) + ->setProject($project) ->trigger(); $queueForEvents->setPayload( @@ -1652,6 +1702,81 @@ App::post('/v1/account/jwt') ])]), Response::MODEL_JWT); }); +App::post('/v1/account/targets/push') + ->desc('Create Account\'s push target') + ->groups(['api', 'account']) + ->label('error', __DIR__ . '/../../views/general/error.phtml') + ->label('audits.event', 'target.create') + ->label('audits.resource', 'target/response.$id') + ->label('event', 'users.[userId].targets.[targetId].create') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'createPushTarget') + ->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.)') + ->inject('queueForEvents') + ->inject('user') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $targetId, string $providerId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + $targetId = $targetId == 'unique()' ? ID::unique() : $targetId; + + $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); + } + + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + + if (!$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + $detector = new Detector($request->getUserAgent()); + $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $device = $detector->getDevice(); + + try { + $target = $dbForProject->createDocument('targets', new Document([ + '$id' => $targetId, + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::update(Role::user($user->getId())), + ], + 'providerId' => $providerId ?? null, + 'providerInternalId' => $provider->getInternalId() ?? null, + 'providerType' => MESSAGE_TYPE_PUSH, + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'identifier' => $identifier, + 'name' => "{$device['deviceBrand']} {$device['deviceModel']}" + ])); + } catch (Duplicate) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('targetId', $target->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($target, Response::MODEL_TARGET); + }); + App::get('/v1/account') ->desc('Get account') ->groups(['api', 'account']) @@ -1977,6 +2102,7 @@ App::patch('/v1/account/email') throw new Exception(Exception::USER_INVALID_CREDENTIALS); } + $oldEmail = $user->getAttribute('email'); $email = \strtolower($email); // Makes sure this email is not already used in another identity @@ -2001,8 +2127,25 @@ App::patch('/v1/account/email') ->setAttribute('passwordUpdate', DateTime::now()); } + $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ])); + + if ($target instanceof Document && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + try { $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldEmail, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email))); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -2047,6 +2190,16 @@ App::patch('/v1/account/phone') throw new Exception(Exception::USER_INVALID_CREDENTIALS); } + $target = Authorization::skip(fn () => $dbForProject->findOne('targets', [ + Query::equal('identifier', [$phone]), + ])); + + if ($target instanceof Document && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + $oldPhone = $user->getAttribute('phone'); + $user ->setAttribute('phone', $phone) ->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again @@ -2062,6 +2215,15 @@ App::patch('/v1/account/phone') try { $user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user)); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldPhone, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone))); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate $th) { throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS); } @@ -2272,8 +2434,8 @@ App::patch('/v1/account/sessions/:sessionId') $provider = $session->getAttribute('provider'); $refreshToken = $session->getAttribute('providerRefreshToken'); - $appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? ''; - $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}'; + $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? ''; + $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}'; $className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider); @@ -2570,8 +2732,8 @@ App::put('/v1/account/recovery') ->label('abuse-key', 'url:{url},userId:{param-userId}') ->param('userId', '', new UID(), 'User ID.') ->param('secret', '', new Text(256), 'Valid reset token.') - ->param('password', '', new Password(), 'New user password. Must be at least 8 chars.') - ->param('passwordAgain', '', new Password(), 'Repeat new user password. Must be at least 8 chars.') + ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary']) + ->param('passwordAgain', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Repeat new user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary']) ->inject('response') ->inject('user') ->inject('dbForProject') @@ -2885,10 +3047,9 @@ App::post('/v1/account/verification/phone') ->inject('queueForMessaging') ->inject('project') ->inject('locale') - ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, EventPhone $queueForMessaging, Document $project, Locale $locale) { - + ->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale) { if (empty(App::getEnv('_APP_SMS_PROVIDER'))) { - throw new Exception(Exception::GENERAL_PHONE_DISABLED); + throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); } if (empty($user->getAttribute('phone'))) { @@ -2937,11 +3098,19 @@ App::post('/v1/account/verification/phone') $message = $message->setParam('{{token}}', $secret); $message = $message->render(); + $messageDoc = new Document([ + '$id' => $verification->getId(), + 'data' => [ + 'content' => $message, + ], + ]); + $queueForMessaging - ->setRecipient($user->getAttribute('phone')) - ->setMessage($message) - ->trigger() - ; + ->setMessage($messageDoc) + ->setRecipients([$user->getAttribute('phone')]) + ->setProviderType(MESSAGE_TYPE_SMS) + ->setProject($project) + ->trigger(); $queueForEvents ->setParam('userId', $user->getId()) @@ -3018,3 +3187,61 @@ App::put('/v1/account/verification/phone') $response->dynamic($verificationDocument, Response::MODEL_TOKEN); }); + +App::put('/v1/account/targets/:targetId/push') + ->desc('Update Account\'s push target') + ->groups(['api', 'account']) + ->label('error', __DIR__ . '/../../views/general/error.phtml') + ->label('audits.event', 'target.update') + ->label('audits.resource', 'target/response.$id') + ->label('event', 'users.[userId].targets.[targetId].update') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'updatePushTarget') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TARGET) + ->label('docs', false) + ->param('targetId', '', new UID(), 'Target ID.') + ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)') + ->inject('queueForEvents') + ->inject('user') + ->inject('request') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $targetId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) { + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + + if ($target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($user->getId() !== $target->getAttribute('userId')) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($identifier) { + $target->setAttribute('identifier', $identifier); + } + + $detector = new Detector($request->getUserAgent()); + $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $device = $detector->getDevice(); + + $target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}"); + + $target = $dbForProject->updateDocument('targets', $target->getId(), $target); + $dbForProject->deleteCachedDocument('users', $user->getId()); + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('targetId', $target->getId()); + + $response + ->dynamic($target, Response::MODEL_TARGET); + }); diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index e0d967eb00..b6395774e9 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -84,8 +84,8 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro $accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry'); $refreshToken = $gitHubSession->getAttribute('providerRefreshToken'); - $appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? ''; - $appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}'; + $appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? ''; + $appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}'; $className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider); diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 0d2e2f22d1..b3104f6736 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -1561,7 +1561,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti ->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).') ->param('key', '', new Key(), 'Attribute Key.') ->param('required', null, new Boolean(), 'Is attribute required?') - ->param('default', null, new DatetimeValidator(), 'Default value for the attribute in ISO 8601 format. Cannot be set when attribute is required.', true) + ->param('default', null, new DatetimeValidator(), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.', true) ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForProject') diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php new file mode 100644 index 0000000000..124bc2d219 --- /dev/null +++ b/app/controllers/api/messaging.php @@ -0,0 +1,2961 @@ +desc('Create Mailgun provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.create') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].create') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createMailgunProvider') + ->label('sdk.description', '/docs/references/messaging/create-mailgun-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER) + ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Provider name.') + ->param('from', '', new Email(), 'Sender email address.', true) + ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) + ->param('domain', '', new Text(0), 'Mailgun Domain.', true) + ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, string $from, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if ($isEuRegion === true || $isEuRegion === false) { + $credentials['isEuRegion'] = $isEuRegion; + } + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + if (!empty($domain)) { + $credentials['domain'] = $domain; + } + + if ( + $enabled === true && + \array_key_exists('isEuRegion', $credentials) && + \array_key_exists('apiKey', $credentials) && + \array_key_exists('domain', $credentials) && + \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider = new Document([ + '$id' => $providerId, + 'name' => $name, + 'provider' => 'mailgun', + 'type' => MESSAGE_TYPE_EMAIL, + 'enabled' => $enabled, + 'credentials' => $credentials, + 'options' => $options, + ]); + + try { + $provider = $dbForProject->createDocument('providers', $provider); + } catch (DuplicateException) { + throw new Exception(Exception::PROVIDER_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::post('/v1/messaging/providers/sendgrid') + ->desc('Create Sendgrid provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.create') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].create') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createSendgridProvider') + ->label('sdk.description', '/docs/references/messaging/create-sengrid-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER) + ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Provider name.') + ->param('from', '', new Email(), 'Sender email address.', true) + ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, string $from, string $apiKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + if ( + $enabled === true + && \array_key_exists('apiKey', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider = new Document([ + '$id' => $providerId, + 'name' => $name, + 'provider' => 'sendgrid', + 'type' => MESSAGE_TYPE_EMAIL, + 'enabled' => $enabled, + 'credentials' => $credentials, + 'options' => $options, + ]); + + try { + $provider = $dbForProject->createDocument('providers', $provider); + } catch (DuplicateException) { + throw new Exception(Exception::PROVIDER_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::post('/v1/messaging/providers/msg91') + ->desc('Create Msg91 provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.create') + ->label('audits.resource', 'provider/{response.$id}') + ->label('scope', 'providers.write') + ->label('event', 'providers.[providerId].create') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createMsg91Provider') + ->label('sdk.description', '/docs/references/messaging/create-msg91-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER) + ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Provider name.') + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('senderId', '', new Text(0), 'Msg91 Sender ID.', true) + ->param('authKey', '', new Text(0), 'Msg91 Auth Key.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, string $from, string $senderId, string $authKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($senderId)) { + $credentials['senderId'] = $senderId; + } + + if (!empty($authKey)) { + $credentials['authKey'] = $authKey; + } + + if ( + $enabled === true + && \array_key_exists('senderId', $credentials) + && \array_key_exists('authKey', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider = new Document([ + '$id' => $providerId, + 'name' => $name, + 'provider' => 'msg91', + 'type' => MESSAGE_TYPE_SMS, + 'enabled' => $enabled, + 'credentials' => $credentials, + 'options' => $options, + ]); + + try { + $provider = $dbForProject->createDocument('providers', $provider); + } catch (DuplicateException) { + throw new Exception(Exception::PROVIDER_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::post('/v1/messaging/providers/telesign') + ->desc('Create Telesign provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.create') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].create') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createTelesignProvider') + ->label('sdk.description', '/docs/references/messaging/create-telesign-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER) + ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Provider name.') + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('username', '', new Text(0), 'Telesign username.', true) + ->param('password', '', new Text(0), 'Telesign password.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, string $from, string $username, string $password, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($username)) { + $credentials['username'] = $username; + } + + if (!empty($password)) { + $credentials['password'] = $password; + } + + if ( + $enabled === true + && \array_key_exists('username', $credentials) + && \array_key_exists('password', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider = new Document([ + '$id' => $providerId, + 'name' => $name, + 'provider' => 'telesign', + 'type' => MESSAGE_TYPE_SMS, + 'enabled' => $enabled, + 'credentials' => $credentials, + 'options' => $options, + ]); + + try { + $provider = $dbForProject->createDocument('providers', $provider); + } catch (DuplicateException) { + throw new Exception(Exception::PROVIDER_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::post('/v1/messaging/providers/textmagic') + ->desc('Create Textmagic provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.create') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].create') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createTextmagicProvider') + ->label('sdk.description', '/docs/references/messaging/create-textmagic-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER) + ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Provider name.') + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('username', '', new Text(0), 'Textmagic username.', true) + ->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, string $from, string $username, string $apiKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($username)) { + $credentials['username'] = $username; + } + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + if ( + $enabled === true + && \array_key_exists('username', $credentials) + && \array_key_exists('apiKey', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider = new Document([ + '$id' => $providerId, + 'name' => $name, + 'provider' => 'textmagic', + 'type' => MESSAGE_TYPE_SMS, + 'enabled' => $enabled, + 'credentials' => $credentials, + 'options' => $options, + ]); + + try { + $provider = $dbForProject->createDocument('providers', $provider); + } catch (DuplicateException) { + throw new Exception(Exception::PROVIDER_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::post('/v1/messaging/providers/twilio') + ->desc('Create Twilio provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.create') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].create') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createTwilioProvider') + ->label('sdk.description', '/docs/references/messaging/create-twilio-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER) + ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Provider name.') + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('accountSid', '', new Text(0), 'Twilio account secret ID.', true) + ->param('authToken', '', new Text(0), 'Twilio authentication token.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, string $from, string $accountSid, string $authToken, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($accountSid)) { + $credentials['accountSid'] = $accountSid; + } + + if (!empty($authToken)) { + $credentials['authToken'] = $authToken; + } + + if ( + $enabled === true + && \array_key_exists('accountSid', $credentials) + && \array_key_exists('authToken', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider = new Document([ + '$id' => $providerId, + 'name' => $name, + 'provider' => 'twilio', + 'type' => MESSAGE_TYPE_SMS, + 'enabled' => $enabled, + 'credentials' => $credentials, + 'options' => $options, + ]); + + try { + $provider = $dbForProject->createDocument('providers', $provider); + } catch (DuplicateException) { + throw new Exception(Exception::PROVIDER_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::post('/v1/messaging/providers/vonage') + ->desc('Create Vonage provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.create') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].create') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createVonageProvider') + ->label('sdk.description', '/docs/references/messaging/create-vonage-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER) + ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Provider name.') + ->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) + ->param('apiKey', '', new Text(0), 'Vonage API key.', true) + ->param('apiSecret', '', new Text(0), 'Vonage API secret.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, string $from, string $apiKey, string $apiSecret, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $options = []; + + if (!empty($from)) { + $options ['from'] = $from; + } + + $credentials = []; + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + if (!empty($apiSecret)) { + $credentials['apiSecret'] = $apiSecret; + } + + if ( + $enabled === true + && \array_key_exists('apiKey', $credentials) + && \array_key_exists('apiSecret', $credentials) + && \array_key_exists('from', $options) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider = new Document([ + '$id' => $providerId, + 'name' => $name, + 'provider' => 'vonage', + 'type' => MESSAGE_TYPE_SMS, + 'enabled' => $enabled, + 'credentials' => $credentials, + 'options' => $options, + ]); + + try { + $provider = $dbForProject->createDocument('providers', $provider); + } catch (DuplicateException) { + throw new Exception(Exception::PROVIDER_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::post('/v1/messaging/providers/fcm') + ->desc('Create FCM provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.create') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].create') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createFcmProvider') + ->label('sdk.description', '/docs/references/messaging/create-fcm-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER) + ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Provider name.') + ->param('serverKey', '', new Text(0), 'FCM server key.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, string $serverKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $credentials = []; + + if (!empty($serverKey)) { + $credentials['serverKey'] = $serverKey; + } + + if ($enabled === true && \array_key_exists('serverKey', $credentials)) { + $enabled = true; + } else { + $enabled = false; + } + + $provider = new Document([ + '$id' => $providerId, + 'name' => $name, + 'provider' => 'fcm', + 'type' => MESSAGE_TYPE_PUSH, + 'enabled' => $enabled, + 'credentials' => $credentials + ]); + + try { + $provider = $dbForProject->createDocument('providers', $provider); + } catch (DuplicateException) { + throw new Exception(Exception::PROVIDER_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::post('/v1/messaging/providers/apns') + ->desc('Create APNS provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.create') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].create') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createApnsProvider') + ->label('sdk.description', '/docs/references/messaging/create-apns-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER) + ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('name', '', new Text(128), 'Provider name.') + ->param('authKey', '', new Text(0), 'APNS authentication key.', true) + ->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true) + ->param('teamId', '', new Text(0), 'APNS team ID.', true) + ->param('bundleId', '', new Text(0), 'APNS bundle ID.', true) + ->param('endpoint', '', new Text(0), 'APNS endpoint.', true) + ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; + + $credentials = []; + + if (!empty($authKey)) { + $credentials['authKey'] = $authKey; + } + + if (!empty($authKeyId)) { + $credentials['authKeyId'] = $authKeyId; + } + + if (!empty($teamId)) { + $credentials['teamId'] = $teamId; + } + + if (!empty($bundleId)) { + $credentials['bundleId'] = $bundleId; + } + + if (!empty($endpoint)) { + $credentials['endpoint'] = $endpoint; + } + + if ( + $enabled === true + && \array_key_exists('authKey', $credentials) + && \array_key_exists('authKeyId', $credentials) + && \array_key_exists('teamId', $credentials) + && \array_key_exists('bundleId', $credentials) + && \array_key_exists('endpoint', $credentials) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider = new Document([ + '$id' => $providerId, + 'name' => $name, + 'provider' => 'apns', + 'type' => MESSAGE_TYPE_PUSH, + 'enabled' => $enabled, + 'credentials' => $credentials, + ]); + + try { + $provider = $dbForProject->createDocument('providers', $provider); + } catch (DuplicateException) { + throw new Exception(Exception::PROVIDER_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::get('/v1/messaging/providers') + ->desc('List providers') + ->groups(['api', 'messaging']) + ->label('scope', 'providers.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'listProviders') + ->label('sdk.description', '/docs/references/messaging/list-providers.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER_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) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->inject('dbForProject') + ->inject('response') + ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { + $queries = Query::parseQueries($queries); + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + // Get cursor document if there was a cursor query + $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = reset($cursor); + + if ($cursor) { + $providerId = $cursor->getValue(); + $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId)); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Provider '{$providerId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $response->dynamic(new Document([ + 'providers' => $dbForProject->find('providers', $queries), + 'total' => $dbForProject->count('providers', $queries, APP_LIMIT_COUNT), + ]), Response::MODEL_PROVIDER_LIST); + }); + +App::get('/v1/messaging/providers/:providerId/logs') + ->desc('List provider logs') + ->groups(['api', 'messaging']) + ->label('scope', 'providers.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'listProviderLogs') + ->label('sdk.description', '/docs/references/messaging/providers/get-logs.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_LOG_LIST) + ->param('providerId', '', new UID(), 'Provider ID.') + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) + ->inject('response') + ->inject('dbForProject') + ->inject('locale') + ->inject('geodb') + ->action(function (string $providerId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + + $queries = Query::parseQueries($queries); + $grouped = Query::groupByType($queries); + $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; + $offset = $grouped['offset'] ?? 0; + + $audit = new Audit($dbForProject); + $resource = 'provider/' . $providerId; + $logs = $audit->getLogsByResource($resource, $limit, $offset); + $output = []; + + foreach ($logs as $i => &$log) { + $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; + + $detector = new Detector($log['userAgent']); + $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $os = $detector->getOS(); + $client = $detector->getClient(); + $device = $detector->getDevice(); + + $output[$i] = new Document([ + 'event' => $log['event'], + 'userId' => ID::custom($log['data']['userId']), + 'userEmail' => $log['data']['userEmail'] ?? null, + 'userName' => $log['data']['userName'] ?? null, + 'mode' => $log['data']['mode'] ?? null, + 'ip' => $log['ip'], + 'time' => $log['time'], + 'osCode' => $os['osCode'], + 'osName' => $os['osName'], + 'osVersion' => $os['osVersion'], + 'clientType' => $client['clientType'], + 'clientCode' => $client['clientCode'], + 'clientName' => $client['clientName'], + 'clientVersion' => $client['clientVersion'], + 'clientEngine' => $client['clientEngine'], + 'clientEngineVersion' => $client['clientEngineVersion'], + 'deviceName' => $device['deviceName'], + 'deviceBrand' => $device['deviceBrand'], + 'deviceModel' => $device['deviceModel'] + ]); + + $record = $geodb->get($log['ip']); + + if ($record) { + $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--'; + $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown')); + } else { + $output[$i]['countryCode'] = '--'; + $output[$i]['countryName'] = $locale->getText('locale.country.unknown'); + } + } + + $response->dynamic(new Document([ + 'total' => $audit->countLogsByResource($resource), + 'logs' => $output, + ]), Response::MODEL_LOG_LIST); + }); + +App::get('/v1/messaging/providers/:providerId') + ->desc('Get provider') + ->groups(['api', 'messaging']) + ->label('scope', 'providers.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'getProvider') + ->label('sdk.description', '/docs/references/messaging/get-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROVIDER) + ->param('providerId', '', new UID(), 'Provider ID.') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + + $response->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::patch('/v1/messaging/providers/mailgun/:providerId') + ->desc('Update Mailgun provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.update') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].update') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateMailgunProvider') + ->label('sdk.description', '/docs/references/messaging/update-mailgun-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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) + ->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) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + $providerAttr = $provider->getAttribute('provider'); + + if ($providerAttr !== 'mailgun') { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + if (!empty($name)) { + $provider->setAttribute('name', $name); + } + + if (!empty($from)) { + $provider->setAttribute('options', [ + 'from' => $from, + ]); + } + + $credentials = $provider->getAttribute('credentials'); + + if ($isEuRegion === true || $isEuRegion === false) { + $credentials['isEuRegion'] = $isEuRegion; + } + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + if (!empty($domain)) { + $credentials['domain'] = $domain; + } + + $provider->setAttribute('credentials', $credentials); + + if ($enabled === true || $enabled === false) { + if ( + $enabled === true && + \array_key_exists('isEuRegion', $credentials) && + \array_key_exists('apiKey', $credentials) && + \array_key_exists('domain', $credentials) && + \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider->setAttribute('enabled', $enabled); + } + + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::patch('/v1/messaging/providers/sendgrid/:providerId') + ->desc('Update Sendgrid provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.update') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].update') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateSendgridProvider') + ->label('sdk.description', '/docs/references/messaging/update-sendgrid-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('apiKey', '', new Text(0), 'Sendgrid API key.', true) + ->param('from', '', new Email(), 'Sender email address.', 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) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + $providerAttr = $provider->getAttribute('provider'); + + if ($providerAttr !== 'sendgrid') { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + if (!empty($name)) { + $provider->setAttribute('name', $name); + } + + if (!empty($from)) { + $provider->setAttribute('options', [ + 'from' => $from, + ]); + } + + if (!empty($apiKey)) { + $provider->setAttribute('credentials', [ + 'apiKey' => $apiKey, + ]); + } + + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('apiKey', $provider->getAttribute('credentials')) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider->setAttribute('enabled', $enabled); + } + + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::patch('/v1/messaging/providers/msg91/:providerId') + ->desc('Update Msg91 provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.update') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].update') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateMsg91Provider') + ->label('sdk.description', '/docs/references/messaging/update-msg91-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('senderId', '', new Text(0), 'Msg91 Sender ID.', true) + ->param('authKey', '', new Text(0), 'Msg91 Auth Key.', true) + ->param('from', '', new Text(256), 'Sender number.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, ?bool $enabled, string $senderId, string $authKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + $providerAttr = $provider->getAttribute('provider'); + + if ($providerAttr !== 'msg91') { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + if (!empty($name)) { + $provider->setAttribute('name', $name); + } + + if (!empty($from)) { + $provider->setAttribute('options', [ + 'from' => $from, + ]); + } + + $credentials = $provider->getAttribute('credentials'); + + if (!empty($senderId)) { + $credentials['senderId'] = $senderId; + } + + if (!empty($authKey)) { + $credentials['authKey'] = $authKey; + } + + $provider->setAttribute('credentials', $credentials); + + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('senderId', $credentials) + && \array_key_exists('authKey', $credentials) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + $provider->setAttribute('enabled', $enabled); + } + + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::patch('/v1/messaging/providers/telesign/:providerId') + ->desc('Update Telesign provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.update') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].update') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateTelesignProvider') + ->label('sdk.description', '/docs/references/messaging/update-telesign-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('username', '', new Text(0), 'Telesign username.', true) + ->param('password', '', new Text(0), 'Telesign password.', true) + ->param('from', '', new Text(256), 'Sender number.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $password, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + $providerAttr = $provider->getAttribute('provider'); + + if ($providerAttr !== 'telesign') { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + if (!empty($name)) { + $provider->setAttribute('name', $name); + } + + if (!empty($from)) { + $provider->setAttribute('options', [ + 'from' => $from, + ]); + } + + $credentials = $provider->getAttribute('credentials'); + + if (!empty($username)) { + $credentials['username'] = $username; + } + + if (!empty($password)) { + $credentials['password'] = $password; + } + + $provider->setAttribute('credentials', $credentials); + + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('username', $credentials) + && \array_key_exists('password', $credentials) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::patch('/v1/messaging/providers/textmagic/:providerId') + ->desc('Update Textmagic provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.update') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].update') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateTextmagicProvider') + ->label('sdk.description', '/docs/references/messaging/update-textmagic-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('username', '', new Text(0), 'Textmagic username.', true) + ->param('apiKey', '', new Text(0), 'Textmagic apiKey.', true) + ->param('from', '', new Text(256), 'Sender number.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, ?bool $enabled, string $username, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + $providerAttr = $provider->getAttribute('provider'); + + if ($providerAttr !== 'textmagic') { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + if (!empty($name)) { + $provider->setAttribute('name', $name); + } + + if (!empty($from)) { + $provider->setAttribute('options', [ + 'from' => $from, + ]); + } + + $credentials = $provider->getAttribute('credentials'); + + if (!empty($username)) { + $credentials['username'] = $username; + } + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + $provider->setAttribute('credentials', $credentials); + + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('username', $credentials) + && \array_key_exists('apiKey', $credentials) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::patch('/v1/messaging/providers/twilio/:providerId') + ->desc('Update Twilio provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.update') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].update') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateTwilioProvider') + ->label('sdk.description', '/docs/references/messaging/update-twilio-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('accountSid', '', new Text(0), 'Twilio account secret ID.', true) + ->param('authToken', '', new Text(0), 'Twilio authentication token.', true) + ->param('from', '', new Text(256), 'Sender number.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, ?bool $enabled, string $accountSid, string $authToken, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + $providerAttr = $provider->getAttribute('provider'); + + if ($providerAttr !== 'twilio') { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + if (!empty($name)) { + $provider->setAttribute('name', $name); + } + + if (!empty($from)) { + $provider->setAttribute('options', [ + 'from' => $from, + ]); + } + + $credentials = $provider->getAttribute('credentials'); + + if (!empty($accountSid)) { + $credentials['accountSid'] = $accountSid; + } + + if (!empty($authToken)) { + $credentials['authToken'] = $authToken; + } + + $provider->setAttribute('credentials', $credentials); + + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('accountSid', $credentials) + && \array_key_exists('authToken', $credentials) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::patch('/v1/messaging/providers/vonage/:providerId') + ->desc('Update Vonage provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.update') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].update') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateVonageProvider') + ->label('sdk.description', '/docs/references/messaging/update-vonage-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('apiKey', '', new Text(0), 'Vonage API key.', true) + ->param('apiSecret', '', new Text(0), 'Vonage API secret.', true) + ->param('from', '', new Text(256), 'Sender number.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $apiSecret, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + $providerAttr = $provider->getAttribute('provider'); + + if ($providerAttr !== 'vonage') { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + if (!empty($name)) { + $provider->setAttribute('name', $name); + } + + if (!empty($from)) { + $provider->setAttribute('options', [ + 'from' => $from, + ]); + } + + $credentials = $provider->getAttribute('credentials'); + + if (!empty($apiKey)) { + $credentials['apiKey'] = $apiKey; + } + + if (!empty($apiSecret)) { + $credentials['apiSecret'] = $apiSecret; + } + + $provider->setAttribute('credentials', $credentials); + + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('apiKey', $credentials) + && \array_key_exists('apiSecret', $credentials) + && \array_key_exists('from', $provider->getAttribute('options')) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::patch('/v1/messaging/providers/fcm/:providerId') + ->desc('Update FCM provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.update') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].update') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateFcmProvider') + ->label('sdk.description', '/docs/references/messaging/update-fcm-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('serverKey', '', new Text(0), 'FCM Server Key.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, ?bool $enabled, string $serverKey, Event $queueForEvents, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + $providerAttr = $provider->getAttribute('provider'); + + if ($providerAttr !== 'fcm') { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + if (!empty($name)) { + $provider->setAttribute('name', $name); + } + + if (!empty($serverKey)) { + $provider->setAttribute('credentials', ['serverKey' => $serverKey]); + } + + if ($enabled === true || $enabled === false) { + if ($enabled === true && \array_key_exists('serverKey', $provider->getAttribute('credentials'))) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + + +App::patch('/v1/messaging/providers/apns/:providerId') + ->desc('Update APNS provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.update') + ->label('audits.resource', 'provider/{response.$id}') + ->label('event', 'providers.[providerId].update') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateApnsProvider') + ->label('sdk.description', '/docs/references/messaging/update-apns-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('authKey', '', new Text(0), 'APNS authentication key.', true) + ->param('authKeyId', '', new Text(0), 'APNS authentication key ID.', true) + ->param('teamId', '', new Text(0), 'APNS team ID.', true) + ->param('bundleId', '', new Text(0), 'APNS bundle ID.', true) + ->param('endpoint', '', new Text(0), 'APNS endpoint.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, string $name, ?bool $enabled, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, Event $queueForEvents, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + $providerAttr = $provider->getAttribute('provider'); + + if ($providerAttr !== 'apns') { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + if (!empty($name)) { + $provider->setAttribute('name', $name); + } + + $credentials = $provider->getAttribute('credentials'); + + if (!empty($authKey)) { + $credentials['authKey'] = $authKey; + } + + if (!empty($authKeyId)) { + $credentials['authKeyId'] = $authKeyId; + } + + if (!empty($teamId)) { + $credentials['teamId'] = $teamId; + } + + if (!empty($bundleId)) { + $credentials['bundle'] = $bundleId; + } + + if (!empty($endpoint)) { + $credentials['endpoint'] = $endpoint; + } + + $provider->setAttribute('credentials', $credentials); + + if ($enabled === true || $enabled === false) { + if ( + $enabled === true + && \array_key_exists('authKey', $credentials) + && \array_key_exists('authKeyId', $credentials) + && \array_key_exists('teamId', $credentials) + && \array_key_exists('bundleId', $credentials) + && \array_key_exists('endpoint', $credentials) + ) { + $enabled = true; + } else { + $enabled = false; + } + + $provider->setAttribute('enabled', $enabled); + } + + $provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider); + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->dynamic($provider, Response::MODEL_PROVIDER); + }); + +App::delete('/v1/messaging/providers/:providerId') + ->desc('Delete provider') + ->groups(['api', 'messaging']) + ->label('audits.event', 'provider.delete') + ->label('audits.resource', 'provider/{request.$providerId}') + ->label('event', 'providers.[providerId].delete') + ->label('scope', 'providers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'deleteProvider') + ->label('sdk.description', '/docs/references/messaging/delete-provider.md') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_NONE) + ->param('providerId', '', new UID(), 'Provider ID.') + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $providerId, Event $queueForEvents, Database $dbForProject, Response $response) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + + $dbForProject->deleteDocument('providers', $provider->getId()); + + $queueForEvents + ->setParam('providerId', $provider->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_NOCONTENT) + ->noContent(); + }); + +App::post('/v1/messaging/topics') + ->desc('Create a topic.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'topic.create') + ->label('audits.resource', 'topic/{response.$id}') + ->label('event', 'topics.[topicId].create') + ->label('scope', 'topics.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createTopic') + ->label('sdk.description', '/docs/references/messaging/create-topic.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TOPIC) + ->param('topicId', '', new CustomId(), 'Topic ID. Choose a custom Topic ID or a new Topic ID.') + ->param('name', '', new Text(128), 'Topic Name.') + ->param('description', '', new Text(2048), 'Topic Description.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $topicId, string $name, string $description, Event $queueForEvents, Database $dbForProject, Response $response) { + $topicId = $topicId == 'unique()' ? ID::unique() : $topicId; + + $topic = new Document([ + '$id' => $topicId, + 'name' => $name, + ]); + + if ($description) { + $topic->setAttribute('description', $description); + } + + try { + $topic = $dbForProject->createDocument('topics', $topic); + } catch (DuplicateException) { + throw new Exception(Exception::TOPIC_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('topicId', $topic->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($topic, Response::MODEL_TOPIC); + }); + +App::get('/v1/messaging/topics') + ->desc('List topics.') + ->groups(['api', 'messaging']) + ->label('scope', 'topics.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'listTopics') + ->label('sdk.description', '/docs/references/messaging/list-topics.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TOPIC_LIST) + ->param('queries', [], new Topics(), '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(', ', Topics::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->inject('dbForProject') + ->inject('response') + ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { + $queries = Query::parseQueries($queries); + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + // Get cursor document if there was a cursor query + $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = reset($cursor); + + if ($cursor) { + $topicId = $cursor->getValue(); + $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Topic '{$topicId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument[0]); + } + + $response->dynamic(new Document([ + 'topics' => $dbForProject->find('topics', $queries), + 'total' => $dbForProject->count('topics', $queries, APP_LIMIT_COUNT), + ]), Response::MODEL_TOPIC_LIST); + }); + +App::get('/v1/messaging/topics/:topicId/logs') + ->desc('List topic logs') + ->groups(['api', 'messaging']) + ->label('scope', 'topics.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'listTopicLogs') + ->label('sdk.description', '/docs/references/messaging/topics/get-logs.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_LOG_LIST) + ->param('topicId', '', new UID(), 'Topic ID.') + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) + ->inject('response') + ->inject('dbForProject') + ->inject('locale') + ->inject('geodb') + ->action(function (string $topicId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + $topic = $dbForProject->getDocument('topics', $topicId); + + if ($topic->isEmpty()) { + throw new Exception(Exception::TOPIC_NOT_FOUND); + } + + $queries = Query::parseQueries($queries); + $grouped = Query::groupByType($queries); + $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; + $offset = $grouped['offset'] ?? 0; + + $audit = new Audit($dbForProject); + $resource = 'topic/' . $topicId; + $logs = $audit->getLogsByResource($resource, $limit, $offset); + + $output = []; + + foreach ($logs as $i => &$log) { + $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; + + $detector = new Detector($log['userAgent']); + $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $os = $detector->getOS(); + $client = $detector->getClient(); + $device = $detector->getDevice(); + + $output[$i] = new Document([ + 'event' => $log['event'], + 'userId' => ID::custom($log['data']['userId']), + 'userEmail' => $log['data']['userEmail'] ?? null, + 'userName' => $log['data']['userName'] ?? null, + 'mode' => $log['data']['mode'] ?? null, + 'ip' => $log['ip'], + 'time' => $log['time'], + 'osCode' => $os['osCode'], + 'osName' => $os['osName'], + 'osVersion' => $os['osVersion'], + 'clientType' => $client['clientType'], + 'clientCode' => $client['clientCode'], + 'clientName' => $client['clientName'], + 'clientVersion' => $client['clientVersion'], + 'clientEngine' => $client['clientEngine'], + 'clientEngineVersion' => $client['clientEngineVersion'], + 'deviceName' => $device['deviceName'], + 'deviceBrand' => $device['deviceBrand'], + 'deviceModel' => $device['deviceModel'] + ]); + + $record = $geodb->get($log['ip']); + + if ($record) { + $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--'; + $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown')); + } else { + $output[$i]['countryCode'] = '--'; + $output[$i]['countryName'] = $locale->getText('locale.country.unknown'); + } + } + + $response->dynamic(new Document([ + 'total' => $audit->countLogsByResource($resource), + 'logs' => $output, + ]), Response::MODEL_LOG_LIST); + }); + +App::get('/v1/messaging/topics/:topicId') + ->desc('Get a topic.') + ->groups(['api', 'messaging']) + ->label('scope', 'topics.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'getTopic') + ->label('sdk.description', '/docs/references/messaging/get-topic.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TOPIC) + ->param('topicId', '', new UID(), 'Topic ID.') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $topicId, Database $dbForProject, Response $response) { + $topic = $dbForProject->getDocument('topics', $topicId); + + if ($topic->isEmpty()) { + throw new Exception(Exception::TOPIC_NOT_FOUND); + } + + $topic = $dbForProject->getDocument('topics', $topicId); + + $response + ->dynamic($topic, Response::MODEL_TOPIC); + }); + +App::patch('/v1/messaging/topics/:topicId') + ->desc('Update a topic.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'topic.update') + ->label('audits.resource', 'topic/{response.$id}') + ->label('event', 'topics.[topicId].update') + ->label('scope', 'topics.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateTopic') + ->label('sdk.description', '/docs/references/messaging/update-topic.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TOPIC) + ->param('topicId', '', new UID(), 'Topic ID.') + ->param('name', '', new Text(128), 'Topic Name.', true) + ->param('description', '', new Text(2048), 'Topic Description.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $topicId, string $name, string $description, Event $queueForEvents, Database $dbForProject, Response $response) { + $topic = $dbForProject->getDocument('topics', $topicId); + + if ($topic->isEmpty()) { + throw new Exception(Exception::TOPIC_NOT_FOUND); + } + + if (!empty($name)) { + $topic->setAttribute('name', $name); + } + + if (!empty($description)) { + $topic->setAttribute('description', $description); + } + + $topic = $dbForProject->updateDocument('topics', $topicId, $topic); + + $queueForEvents + ->setParam('topicId', $topic->getId()); + + $response + ->dynamic($topic, Response::MODEL_TOPIC); + }); + +App::delete('/v1/messaging/topics/:topicId') + ->desc('Delete a topic.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'topic.delete') + ->label('audits.resource', 'topic/{request.$topicId}') + ->label('event', 'topics.[topicId].delete') + ->label('scope', 'topics.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'deleteTopic') + ->label('sdk.description', '/docs/references/messaging/delete-topic.md') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_NONE) + ->param('topicId', '', new UID(), 'Topic ID.') + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('queueForDeletes') + ->inject('response') + ->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, Delete $queueForDeletes, Response $response) { + $topic = $dbForProject->getDocument('topics', $topicId); + + if ($topic->isEmpty()) { + throw new Exception(Exception::TOPIC_NOT_FOUND); + } + + $dbForProject->deleteDocument('topics', $topicId); + + $queueForDeletes + ->setType(DELETE_TYPE_TOPIC) + ->setDocument($topic); + + $queueForEvents + ->setParam('topicId', $topic->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_NOCONTENT) + ->noContent(); + }); + +App::post('/v1/messaging/topics/:topicId/subscribers') + ->desc('Create a subscriber.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'subscriber.create') + ->label('audits.resource', 'subscriber/{response.$id}') + ->label('event', 'topics.[topicId].subscribers.[subscriberId].create') + ->label('scope', 'subscribers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_JWT, APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createSubscriber') + ->label('sdk.description', '/docs/references/messaging/create-subscriber.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SUBSCRIBER) + ->param('subscriberId', '', new CustomId(), 'Subscriber ID. Choose a custom Subscriber ID or a new Subscriber ID.') + ->param('topicId', '', new UID(), 'Topic ID. The topic ID to subscribe to.') + ->param('targetId', '', new UID(), 'Target ID. The target ID to link to the specified Topic ID.') + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $subscriberId, string $topicId, string $targetId, Event $queueForEvents, Database $dbForProject, Response $response) { + $subscriberId = $subscriberId == 'unique()' ? ID::unique() : $subscriberId; + + $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + + if ($topic->isEmpty()) { + throw new Exception(Exception::TOPIC_NOT_FOUND); + } + + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId)); + + if ($target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + + $subscriber = new Document([ + '$id' => $subscriberId, + '$permissions' => [ + Permission::read(Role::user($user->getId())), + Permission::delete(Role::user($user->getId())), + ], + 'topicId' => $topicId, + 'topicInternalId' => $topic->getInternalId(), + 'targetId' => $targetId, + 'targetInternalId' => $target->getInternalId(), + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => $target->getAttribute('providerType'), + ]); + + try { + $subscriber = $dbForProject->createDocument('subscribers', $subscriber); + Authorization::skip(fn () => $dbForProject->increaseDocumentAttribute('topics', $topicId, 'total', 1)); + } catch (DuplicateException) { + throw new Exception(Exception::SUBSCRIBER_ALREADY_EXISTS); + } + + $queueForEvents + ->setParam('topicId', $topic->getId()) + ->setParam('subscriberId', $subscriber->getId()); + + $subscriber + ->setAttribute('target', $target) + ->setAttribute('userName', $user->getAttribute('name')); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); + }); + +App::get('/v1/messaging/topics/:topicId/subscribers') + ->desc('List subscribers.') + ->groups(['api', 'messaging']) + ->label('scope', 'subscribers.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'listSubscribers') + ->label('sdk.description', '/docs/references/messaging/list-subscribers.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SUBSCRIBER_LIST) + ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.') + ->param('queries', [], new Subscribers(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Providers::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->inject('dbForProject') + ->inject('response') + ->action(function (string $topicId, array $queries, string $search, Database $dbForProject, Response $response) { + $queries = Query::parseQueries($queries); + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + + if ($topic->isEmpty()) { + throw new Exception(Exception::TOPIC_NOT_FOUND); + } + + \array_push($queries, Query::equal('topicInternalId', [$topic->getInternalId()])); + + // Get cursor document if there was a cursor query + $cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]); + $cursor = reset($cursor); + + if ($cursor) { + $subscriberId = $cursor->getValue(); + $cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('subscribers', $subscriberId)); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Subscriber '{$subscriberId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $subscribers = $dbForProject->find('subscribers', $queries); + + $subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) { + return function () use ($subscriber, $dbForProject) { + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + + return $subscriber + ->setAttribute('target', $target) + ->setAttribute('userName', $user->getAttribute('name')); + }; + }, $subscribers)); + + $response + ->dynamic(new Document([ + 'subscribers' => $subscribers, + 'total' => $dbForProject->count('subscribers', $queries, APP_LIMIT_COUNT), + ]), Response::MODEL_SUBSCRIBER_LIST); + }); + +App::get('/v1/messaging/subscribers/:subscriberId/logs') + ->desc('List subscriber logs') + ->groups(['api', 'messaging']) + ->label('scope', 'subscribers.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'listSubscriberLogs') + ->label('sdk.description', '/docs/references/messaging/subscribers/get-logs.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_LOG_LIST) + ->param('subscriberId', '', new UID(), 'Subscriber ID.') + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true) + ->inject('response') + ->inject('dbForProject') + ->inject('locale') + ->inject('geodb') + ->action(function (string $subscriberId, array $queries, Response $response, Database $dbForProject, Locale $locale, Reader $geodb) { + $subscriber = $dbForProject->getDocument('subscribers', $subscriberId); + + if ($subscriber->isEmpty()) { + throw new Exception(Exception::SUBSCRIBER_NOT_FOUND); + } + + $queries = Query::parseQueries($queries); + $grouped = Query::groupByType($queries); + $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; + $offset = $grouped['offset'] ?? 0; + + $audit = new Audit($dbForProject); + $resource = 'subscriber/' . $subscriberId; + $logs = $audit->getLogsByResource($resource, $limit, $offset); + + $output = []; + + foreach ($logs as $i => &$log) { + $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; + + $detector = new Detector($log['userAgent']); + $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $os = $detector->getOS(); + $client = $detector->getClient(); + $device = $detector->getDevice(); + + $output[$i] = new Document([ + 'event' => $log['event'], + 'userId' => ID::custom($log['data']['userId']), + 'userEmail' => $log['data']['userEmail'] ?? null, + 'userName' => $log['data']['userName'] ?? null, + 'mode' => $log['data']['mode'] ?? null, + 'ip' => $log['ip'], + 'time' => $log['time'], + 'osCode' => $os['osCode'], + 'osName' => $os['osName'], + 'osVersion' => $os['osVersion'], + 'clientType' => $client['clientType'], + 'clientCode' => $client['clientCode'], + 'clientName' => $client['clientName'], + 'clientVersion' => $client['clientVersion'], + 'clientEngine' => $client['clientEngine'], + 'clientEngineVersion' => $client['clientEngineVersion'], + 'deviceName' => $device['deviceName'], + 'deviceBrand' => $device['deviceBrand'], + 'deviceModel' => $device['deviceModel'] + ]); + + $record = $geodb->get($log['ip']); + + if ($record) { + $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--'; + $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown')); + } else { + $output[$i]['countryCode'] = '--'; + $output[$i]['countryName'] = $locale->getText('locale.country.unknown'); + } + } + + $response->dynamic(new Document([ + 'total' => $audit->countLogsByResource($resource), + 'logs' => $output, + ]), Response::MODEL_LOG_LIST); + }); + +App::get('/v1/messaging/topics/:topicId/subscribers/:subscriberId') + ->desc('Get a subscriber.') + ->groups(['api', 'messaging']) + ->label('scope', 'subscribers.read') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'getSubscriber') + ->label('sdk.description', '/docs/references/messaging/get-subscriber.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SUBSCRIBER) + ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.') + ->param('subscriberId', '', new UID(), 'Subscriber ID.') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $topicId, string $subscriberId, Database $dbForProject, Response $response) { + $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + + if ($topic->isEmpty()) { + throw new Exception(Exception::TOPIC_NOT_FOUND); + } + + $subscriber = $dbForProject->getDocument('subscribers', $subscriberId); + + if ($subscriber->isEmpty() || $subscriber->getAttribute('topicId') !== $topicId) { + throw new Exception(Exception::SUBSCRIBER_NOT_FOUND); + } + + $target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $subscriber->getAttribute('targetId'))); + $user = Authorization::skip(fn () => $dbForProject->getDocument('users', $target->getAttribute('userId'))); + + $subscriber + ->setAttribute('target', $target) + ->setAttribute('userName', $user->getAttribute('name')); + + $response + ->dynamic($subscriber, Response::MODEL_SUBSCRIBER); + }); + +App::delete('/v1/messaging/topics/:topicId/subscribers/:subscriberId') + ->desc('Delete a subscriber.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'subscriber.delete') + ->label('audits.resource', 'subscriber/{request.$subscriberId}') + ->label('event', 'topics.[topicId].subscribers.[subscriberId].delete') + ->label('scope', 'subscribers.write') + ->label('sdk.auth', [APP_AUTH_TYPE_JWT, APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'deleteSubscriber') + ->label('sdk.description', '/docs/references/messaging/delete-subscriber.md') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_NONE) + ->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.') + ->param('subscriberId', '', new UID(), 'Subscriber ID.') + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $topicId, string $subscriberId, Event $queueForEvents, Database $dbForProject, Response $response) { + $topic = Authorization::skip(fn () => $dbForProject->getDocument('topics', $topicId)); + + if ($topic->isEmpty()) { + throw new Exception(Exception::TOPIC_NOT_FOUND); + } + + $subscriber = $dbForProject->getDocument('subscribers', $subscriberId); + + if ($subscriber->isEmpty() || $subscriber->getAttribute('topicId') !== $topicId) { + throw new Exception(Exception::SUBSCRIBER_NOT_FOUND); + } + + $dbForProject->deleteDocument('subscribers', $subscriberId); + Authorization::skip(fn () => $dbForProject->decreaseDocumentAttribute('topics', $topicId, 'total', 1)); + + $queueForEvents + ->setParam('topicId', $topic->getId()) + ->setParam('subscriberId', $subscriber->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_NOCONTENT) + ->noContent(); + }); + +App::post('/v1/messaging/messages/email') + ->desc('Create an email.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'message.create') + ->label('audits.resource', 'message/{response.$id}') + ->label('event', 'messages.[messageId].create') + ->label('scope', 'messages.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createEmailMessage') + ->label('sdk.description', '/docs/references/messaging/create-email.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('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('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) + ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('project') + ->inject('queueForMessaging') + ->inject('response') + ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, string $description, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; + + if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { + throw new Exception(Exception::MESSAGE_MISSING_TARGET); + } + + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_EMAIL) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL . ' ' . $targetDocument->getId()); + } + } + + $message = $dbForProject->createDocument('messages', new Document([ + '$id' => $messageId, + 'providerType' => MESSAGE_TYPE_EMAIL, + 'topics' => $topics, + 'users' => $users, + 'targets' => $targets, + 'description' => $description, + 'data' => [ + 'subject' => $subject, + 'content' => $content, + 'html' => $html, + ], + 'status' => $status, + ])); + + if ($status === 'processing') { + $queueForMessaging + ->setMessageId($message->getId()) + ->setProject($project) + ->trigger(); + } + + $queueForEvents + ->setParam('messageId', $message->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($message, Response::MODEL_MESSAGE); + }); + +App::post('/v1/messaging/messages/sms') + ->desc('Create an SMS.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'message.create') + ->label('audits.resource', 'message/{response.$id}') + ->label('event', 'messages.[messageId].create') + ->label('scope', 'messages.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createSMSMessage') + ->label('sdk.description', '/docs/references/messaging/create-sms.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('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) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('project') + ->inject('queueForMessaging') + ->inject('response') + ->action(function (string $messageId, string $content, array $topics, array $users, array $targets, string $description, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; + + if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { + throw new Exception(Exception::MESSAGE_MISSING_TARGET); + } + + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_SMS) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS . ' ' . $targetDocument->getId()); + } + } + + $message = $dbForProject->createDocument('messages', new Document([ + '$id' => $messageId, + 'providerType' => MESSAGE_TYPE_SMS, + 'topics' => $topics, + 'users' => $users, + 'targets' => $targets, + 'description' => $description, + 'data' => [ + 'content' => $content, + ], + 'status' => $status, + ])); + + if ($status === 'processing') { + $queueForMessaging + ->setMessageId($message->getId()) + ->setProject($project) + ->trigger(); + } + + $queueForEvents + ->setParam('messageId', $message->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($message, Response::MODEL_MESSAGE); + }); + +App::post('/v1/messaging/messages/push') + ->desc('Create a push notification.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'message.create') + ->label('audits.resource', 'message/{response.$id}') + ->label('event', 'messages.[messageId].create') + ->label('scope', 'messages.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'createPushMessage') + ->label('sdk.description', '/docs/references/messaging/create-push-notification.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->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('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('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) + ->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', 'processing', new WhiteList(['draft', 'canceled', 'processing']), 'Message Status. Value must be either draft or cancelled or processing.', true) + ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) + ->inject('queueForEvents') + ->inject('dbForProject') + ->inject('project') + ->inject('queueForMessaging') + ->inject('response') + ->action(function (string $messageId, string $title, string $body, array $topics, array $users, array $targets, string $description, ?array $data, string $action, string $icon, string $sound, string $color, string $tag, string $badge, string $status, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Document $project, Messaging $queueForMessaging, Response $response) { + $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; + + if (\count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { + throw new Exception(Exception::MESSAGE_MISSING_TARGET); + } + + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH . ' ' . $targetDocument->getId()); + } + } + + $pushData = []; + + $keys = ['title', 'body', 'data', 'action', 'icon', 'sound', 'color', 'tag', 'badge']; + + foreach ($keys as $key) { + if (!empty($$key)) { + $pushData[$key] = $$key; + } + } + + $message = $dbForProject->createDocument('messages', new Document([ + '$id' => $messageId, + 'providerType' => MESSAGE_TYPE_PUSH, + 'topics' => $topics, + 'users' => $users, + 'targets' => $targets, + 'description' => $description, + 'scheduledAt' => $scheduledAt, + 'data' => $pushData, + 'status' => $status, + ])); + + if ($status === 'processing') { + $queueForMessaging + ->setMessageId($message->getId()) + ->setProject($project) + ->trigger(); + } + + $queueForEvents + ->setParam('messageId', $message->getId()); + + $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 Messages(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Providers::ALLOWED_ATTRIBUTES), true) + ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) + ->inject('dbForProject') + ->inject('response') + ->action(function (array $queries, string $search, Database $dbForProject, Response $response) { + $queries = Query::parseQueries($queries); + + if (!empty($search)) { + $queries[] = Query::search('search', $search); + } + + // 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->getDocument('messages', $messageId)); + + if ($cursorDocument->isEmpty()) { + throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Message '{$messageId}' for the 'cursor' value not found."); + } + + $cursor->setValue($cursorDocument); + } + + $response->dynamic(new Document([ + 'messages' => $dbForProject->find('messages', $queries), + 'total' => $dbForProject->count('messages', $queries, APP_LIMIT_COUNT), + ]), Response::MODEL_MESSAGE_LIST); + }); + +App::get('/v1/messaging/messages/:messageId/logs') + ->desc('List message logs') + ->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', 'listMessageLogs') + ->label('sdk.description', '/docs/references/messaging/messages/get-logs.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_LOG_LIST) + ->param('messageId', '', new UID(), 'Message ID.') + ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', 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); + } + + $queries = Query::parseQueries($queries); + $grouped = Query::groupByType($queries); + $limit = $grouped['limit'] ?? APP_LIMIT_COUNT; + $offset = $grouped['offset'] ?? 0; + + $audit = new Audit($dbForProject); + $resource = 'message/' . $messageId; + $logs = $audit->getLogsByResource($resource, $limit, $offset); + + $output = []; + + foreach ($logs as $i => &$log) { + $log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN'; + + $detector = new Detector($log['userAgent']); + $detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then) + + $os = $detector->getOS(); + $client = $detector->getClient(); + $device = $detector->getDevice(); + + $output[$i] = new Document([ + 'event' => $log['event'], + 'userId' => ID::custom($log['data']['userId']), + 'userEmail' => $log['data']['userEmail'] ?? null, + 'userName' => $log['data']['userName'] ?? null, + 'mode' => $log['data']['mode'] ?? null, + 'ip' => $log['ip'], + 'time' => $log['time'], + 'osCode' => $os['osCode'], + 'osName' => $os['osName'], + 'osVersion' => $os['osVersion'], + 'clientType' => $client['clientType'], + 'clientCode' => $client['clientCode'], + 'clientName' => $client['clientName'], + 'clientVersion' => $client['clientVersion'], + 'clientEngine' => $client['clientEngine'], + 'clientEngineVersion' => $client['clientEngineVersion'], + 'deviceName' => $device['deviceName'], + 'deviceBrand' => $device['deviceBrand'], + 'deviceModel' => $device['deviceModel'] + ]); + + $record = $geodb->get($log['ip']); + + if ($record) { + $output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--'; + $output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown')); + } else { + $output[$i]['countryCode'] = '--'; + $output[$i]['countryName'] = $locale->getText('locale.country.unknown'); + } + } + + $response->dynamic(new Document([ + 'total' => $audit->countLogsByResource($resource), + 'logs' => $output, + ]), Response::MODEL_LOG_LIST); + }); + +App::get('/v1/messaging/messages/:messageId') + ->desc('Get a message') + ->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', 'getMessage') + ->label('sdk.description', '/docs/references/messaging/get-message.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_MESSAGE) + ->param('messageId', '', new UID(), 'Message ID.') + ->inject('dbForProject') + ->inject('response') + ->action(function (string $messageId, Database $dbForProject, Response $response) { + $message = $dbForProject->getDocument('messages', $messageId); + + if ($message->isEmpty()) { + throw new Exception(Exception::MESSAGE_NOT_FOUND); + } + + $response->dynamic($message, Response::MODEL_MESSAGE); + }); + +App::patch('/v1/messaging/messages/email/:messageId') + ->desc('Update an email.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'message.update') + ->label('audits.resource', 'message/{response.$id}') + ->label('event', 'messages.[messageId].update') + ->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_OK) + ->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('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) { + $message = $dbForProject->getDocument('messages', $messageId); + + if ($message->isEmpty()) { + throw new Exception(Exception::MESSAGE_NOT_FOUND); + } + + if ($message->getAttribute('status') === 'sent') { + throw new Exception(Exception::MESSAGE_ALREADY_SENT); + } + + if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) { + throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); + } + + if (!\is_null($topics)) { + $message->setAttribute('topics', $topics); + } + + if (!\is_null($users)) { + $message->setAttribute('users', $users); + } + + if (!\is_null($targets)) { + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_EMAIL) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_EMAIL . ' ' . $targetDocument->getId()); + } + } + + $message->setAttribute('targets', $targets); + } + + $data = $message->getAttribute('data'); + + if (!empty($subject)) { + $data['subject'] = $subject; + } + + if (!empty($content)) { + $data['content'] = $content; + } + + if (!empty($html)) { + $data['html'] = $html; + } + + $message->setAttribute('data', $data); + + if (!empty($description)) { + $message->setAttribute('description', $description); + } + + if (!empty($status)) { + $message->setAttribute('status', $status); + } + + if (!is_null($scheduledAt)) { + $message->setAttribute('scheduledAt', $scheduledAt); + } + + $message = $dbForProject->updateDocument('messages', $message->getId(), $message); + + if ($status === 'processing') { + $queueForMessaging + ->setMessageId($message->getId()) + ->setProject($project) + ->trigger(); + } + + $queueForEvents + ->setParam('messageId', $message->getId()); + + $response + ->dynamic($message, Response::MODEL_MESSAGE); + }); + +App::patch('/v1/messaging/messages/sms/:messageId') + ->desc('Update an SMS.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'message.update') + ->label('audits.resource', 'message/{response.$id}') + ->label('event', 'messages.[messageId].update') + ->label('scope', 'messages.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updateSMS') + ->label('sdk.description', '/docs/references/messaging/update-email.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->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('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) { + $message = $dbForProject->getDocument('messages', $messageId); + + if ($message->isEmpty()) { + throw new Exception(Exception::MESSAGE_NOT_FOUND); + } + + if ($message->getAttribute('status') === 'sent') { + throw new Exception(Exception::MESSAGE_ALREADY_SENT); + } + + if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) { + throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); + } + + if (!\is_null($topics)) { + $message->setAttribute('topics', $topics); + } + + if (!\is_null($users)) { + $message->setAttribute('users', $users); + } + + if (!\is_null($targets)) { + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_SMS) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_SMS . ' ' . $targetDocument->getId()); + } + } + + $message->setAttribute('targets', $targets); + } + + $data = $message->getAttribute('data'); + + if (!empty($content)) { + $data['content'] = $content; + } + + $message->setAttribute('data', $data); + + if (!empty($status)) { + $message->setAttribute('status', $status); + } + + if (!empty($description)) { + $message->setAttribute('description', $description); + } + + if (!is_null($scheduledAt)) { + $message->setAttribute('scheduledAt', $scheduledAt); + } + + $message = $dbForProject->updateDocument('messages', $message->getId(), $message); + + if ($status === 'processing') { + $queueForMessaging + ->setMessageId($message->getId()) + ->setProject($project) + ->trigger(); + } + + $queueForEvents + ->setParam('messageId', $message->getId()); + + $response + ->dynamic($message, Response::MODEL_MESSAGE); + }); + +App::patch('/v1/messaging/messages/push/:messageId') + ->desc('Update a push notification.') + ->groups(['api', 'messaging']) + ->label('audits.event', 'message.update') + ->label('audits.resource', 'message/{response.$id}') + ->label('event', 'messages.[messageId].update') + ->label('scope', 'messages.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'messaging') + ->label('sdk.method', 'updatePushNotification') + ->label('sdk.description', '/docs/references/messaging/update-push-notification.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->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('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('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) { + $message = $dbForProject->getDocument('messages', $messageId); + + if ($message->isEmpty()) { + throw new Exception(Exception::MESSAGE_NOT_FOUND); + } + + if ($message->getAttribute('status') === 'sent') { + throw new Exception(Exception::MESSAGE_ALREADY_SENT); + } + + if (!is_null($message->getAttribute('scheduledAt')) && $message->getAttribute('scheduledAt') < new \DateTime()) { + throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); + } + + if (!\is_null($topics)) { + $message->setAttribute('topics', $topics); + } + + if (!\is_null($users)) { + $message->setAttribute('users', $users); + } + + if (!\is_null($targets)) { + foreach ($targets as $target) { + $targetDocument = $dbForProject->getDocument('targets', $target); + + if ($targetDocument->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($targetDocument->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) { + throw new Exception(Exception::MESSAGE_TARGET_NOT_PUSH . ' ' . $targetDocument->getId()); + } + } + + $message->setAttribute('targets', $targets); + } + + $pushData = $message->getAttribute('data'); + + if ($title) { + $pushData['title'] = $title; + } + + if ($body) { + $pushData['body'] = $body; + } + + if (!is_null($data)) { + $pushData['data'] = $data; + } + + if ($action) { + $pushData['action'] = $action; + } + + if ($icon) { + $pushData['icon'] = $icon; + } + + if ($sound) { + $pushData['sound'] = $sound; + } + + if ($color) { + $pushData['color'] = $color; + } + + if ($tag) { + $pushData['tag'] = $tag; + } + + if ($badge) { + $pushData['badge'] = $badge; + } + + $message->setAttribute('data', $pushData); + + if (!empty($status)) { + $message->setAttribute('status', $status); + } + + if (!empty($description)) { + $message->setAttribute('description', $description); + } + + if (!is_null($scheduledAt)) { + $message->setAttribute('scheduledAt', $scheduledAt); + } + + $message = $dbForProject->updateDocument('messages', $message->getId(), $message); + + if ($status === 'processing') { + $queueForMessaging + ->setMessageId($message->getId()) + ->setProject($project) + ->trigger(); + } + + $queueForEvents + ->setParam('messageId', $message->getId()); + + $response + ->dynamic($message, Response::MODEL_MESSAGE); + }); diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index b8f8ac4727..fe441e0e8c 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -159,7 +159,7 @@ App::post('/v1/projects') 'legalTaxId' => ID::custom($legalTaxId), 'services' => new stdClass(), 'platforms' => null, - 'authProviders' => [], + 'oAuthProviders' => [], 'webhooks' => null, 'keys' => null, 'auths' => $auths, @@ -598,7 +598,7 @@ App::patch('/v1/projects/:projectId/oauth2') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_PROJECT) ->param('projectId', '', new UID(), 'Project unique ID.') - ->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'Provider Name') + ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'Provider Name') ->param('appId', null, new Text(256), 'Provider app ID. Max length: 256 chars.', true) ->param('secret', null, new text(512), 'Provider secret key. Max length: 512 chars.', true) ->param('enabled', null, new Boolean(), 'Provider status. Set to \'false\' to disable new session creation.', true) @@ -612,7 +612,7 @@ App::patch('/v1/projects/:projectId/oauth2') throw new Exception(Exception::PROJECT_NOT_FOUND); } - $providers = $project->getAttribute('authProviders', []); + $providers = $project->getAttribute('oAuthProviders', []); if ($appId !== null) { $providers[$provider . 'Appid'] = $appId; @@ -626,7 +626,7 @@ App::patch('/v1/projects/:projectId/oauth2') $providers[$provider . 'Enabled'] = $enabled; } - $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('authProviders', $providers)); + $project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('oAuthProviders', $providers)); $response->dynamic($project, Response::MODEL_PROJECT); }); @@ -1151,7 +1151,7 @@ App::post('/v1/projects/:projectId/keys') ->param('projectId', '', new UID(), 'Project unique ID.') ->param('name', null, new Text(128), 'Key name. Max length: 128 chars.') ->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.') - ->param('expire', null, new DatetimeValidator(), 'Expiration time in ISO 8601 format. Use null for unlimited expiration.', true) + ->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true) ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { @@ -1268,7 +1268,7 @@ App::put('/v1/projects/:projectId/keys/:keyId') ->param('keyId', '', new UID(), 'Key unique ID.') ->param('name', null, new Text(128), 'Key name. Max length: 128 chars.') ->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.') - ->param('expire', null, new DatetimeValidator(), 'Expiration time in ISO 8601 format. Use null for unlimited expiration.', true) + ->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true) ->inject('response') ->inject('dbForConsole') ->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) { diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 2ee351f469..2ba27efcb3 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -6,7 +6,7 @@ use Appwrite\Detector\Detector; use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Event\Mail; -use Appwrite\Event\Phone as EventPhone; +use Appwrite\Event\Messaging; use Appwrite\Extend\Exception; use Appwrite\Network\Validator\Email; use Utopia\Validator\Host; @@ -388,7 +388,7 @@ App::post('/v1/teams/:teamId/memberships') ->inject('queueForMails') ->inject('queueForMessaging') ->inject('queueForEvents') - ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, EventPhone $queueForMessaging, Event $queueForEvents) { + ->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents) { $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -628,6 +628,10 @@ App::post('/v1/teams/:teamId/memberships') ->trigger() ; } elseif (!empty($phone)) { + if (empty(App::getEnv('_APP_SMS_PROVIDER'))) { + throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured'); + } + $message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl'); $customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? []; @@ -638,9 +642,18 @@ App::post('/v1/teams/:teamId/memberships') $message = $message->setParam('{{token}}', $url); $message = $message->render(); + $messageDoc = new Document([ + '$id' => ID::unique(), + 'data' => [ + 'content' => $message, + ], + ]); + $queueForMessaging - ->setRecipient($phone) - ->setMessage($message) + ->setMessage($messageDoc) + ->setRecipients([$phone]) + ->setProviderType('SMS') + ->setProject($project) ->trigger(); } } diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 0869453cc9..8a71f33d8b 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -9,6 +9,7 @@ use Appwrite\Event\Event; use Appwrite\Network\Validator\Email; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Identities; +use Appwrite\Utopia\Database\Validator\Queries\Targets; use Utopia\Database\Validator\Queries; use Appwrite\Utopia\Database\Validator\Queries\Users; use Utopia\Database\Validator\Query\Limit; @@ -98,6 +99,42 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'memberships' => null, 'search' => implode(' ', [$userId, $email, $phone, $name]), ])); + + if ($email) { + try { + $target = $dbForProject->createDocument('targets', new Document([ + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => 'email', + 'identifier' => $email, + ])); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]); + } catch (Duplicate) { + $existingTarget = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ]); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]); + } + } + + if ($phone) { + try { + $target = $dbForProject->createDocument('targets', new Document([ + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'providerType' => 'sms', + 'identifier' => $phone, + ])); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]); + } catch (Duplicate) { + $existingTarget = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$phone]), + ]); + $user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]); + } + } + + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate $th) { throw new Exception(Exception::USER_ALREADY_EXISTS); } @@ -379,6 +416,98 @@ App::post('/v1/users/scrypt-modified') ->dynamic($user, Response::MODEL_USER); }); +App::post('/v1/users/:userId/targets') + ->desc('Create User Target') + ->groups(['api', 'users']) + ->label('audits.event', 'target.create') + ->label('audits.resource', 'target/response.$id') + ->label('event', 'users.[userId].targets.[targetId].create') + ->label('scope', 'targets.write') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'createTarget') + ->label('sdk.description', '/docs/references/users/create-target.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TARGET) + ->param('targetId', '', new CustomId(), 'Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('userId', '', new UID(), 'User ID.') + ->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.') + ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)') + ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true) + ->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true) + ->inject('queueForEvents') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $targetId, string $userId, string $providerType, string $identifier, string $providerId, string $name, Event $queueForEvents, Response $response, Database $dbForProject) { + $targetId = $targetId == 'unique()' ? ID::unique() : $targetId; + + $provider = new Document(); + + if ($providerType === MESSAGE_TYPE_PUSH) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + } + + switch ($providerType) { + case 'email': + $validator = new Email(); + if (!$validator->isValid($identifier)) { + throw new Exception(Exception::GENERAL_INVALID_EMAIL); + } + break; + case MESSAGE_TYPE_SMS: + $validator = new Phone(); + if (!$validator->isValid($identifier)) { + throw new Exception(Exception::GENERAL_INVALID_PHONE); + } + break; + case MESSAGE_TYPE_PUSH: + break; + default: + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $target = $dbForProject->getDocument('targets', $targetId); + + if (!$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + try { + $target = $dbForProject->createDocument('targets', new Document([ + '$id' => $targetId, + 'providerId' => $providerId ?? null, + 'providerInternalId' => $provider->getInternalId() ?? null, + 'providerType' => $providerType, + 'userId' => $userId, + 'userInternalId' => $user->getInternalId(), + 'identifier' => $identifier, + 'name' => ($name !== '') ? $name : null, + ])); + } catch (Duplicate) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('targetId', $target->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($target, Response::MODEL_TARGET); + }); + App::get('/v1/users') ->desc('List users') ->groups(['api', 'users']) @@ -482,6 +611,38 @@ App::get('/v1/users/:userId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); +App::get('/v1/users/:userId/targets/:targetId') + ->desc('Get User Target') + ->groups(['api', 'users']) + ->label('scope', 'targets.read') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'getTarget') + ->label('sdk.description', '/docs/references/users/get-user-target.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TARGET) + ->param('userId', '', new UID(), 'User ID.') + ->param('targetId', '', new UID(), 'Target ID.') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $userId, string $targetId, Response $response, Database $dbForProject) { + + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $target = $user->find('$id', $targetId, 'targets'); + + if (empty($target)) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + $response->dynamic($target, Response::MODEL_TARGET); + }); + App::get('/v1/users/:userId/sessions') ->desc('List user sessions') ->groups(['api', 'users']) @@ -646,6 +807,53 @@ App::get('/v1/users/:userId/logs') ]), Response::MODEL_LOG_LIST); }); +App::get('/v1/users/:userId/targets') + ->desc('List User Targets') + ->groups(['api', 'users']) + ->label('scope', 'targets.read') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'listTargets') + ->label('sdk.description', '/docs/references/users/list-user-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('userId', '', new UID(), 'User 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(', ', Users::ALLOWED_ATTRIBUTES), true) + ->inject('response') + ->inject('dbForProject') + ->action(function (string $userId, array $queries, Response $response, Database $dbForProject) { + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $queries = Query::parseQueries($queries); + + $queries[] = Query::equal('userId', [$userId]); + + // 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/users/identities') ->desc('List Identities') ->groups(['api', 'users']) @@ -949,6 +1157,16 @@ App::patch('/v1/users/:userId/email') throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } + $target = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$email]), + ]); + + if ($target instanceof Document && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + + $oldEmail = $user->getAttribute('email'); + $user ->setAttribute('email', $email) ->setAttribute('emailVerification', false) @@ -957,6 +1175,15 @@ App::patch('/v1/users/:userId/email') try { $user = $dbForProject->updateDocument('users', $user->getId(), $user); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldEmail, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate $th) { throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS); } @@ -994,13 +1221,32 @@ App::patch('/v1/users/:userId/phone') throw new Exception(Exception::USER_NOT_FOUND); } + $oldPhone = $user->getAttribute('phone'); + $user ->setAttribute('phone', $number) ->setAttribute('phoneVerification', false) ; + $target = $dbForProject->findOne('targets', [ + Query::equal('identifier', [$number]), + ]); + + if ($target instanceof Document && !$target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS); + } + try { $user = $dbForProject->updateDocument('users', $user->getId(), $user); + /** + * @var Document $oldTarget + */ + $oldTarget = $user->find('identifier', $oldPhone, 'targets'); + + if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { + $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number)); + } + $dbForProject->deleteCachedDocument('users', $user->getId()); } catch (Duplicate $th) { throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS); } @@ -1080,6 +1326,100 @@ App::patch('/v1/users/:userId/prefs') $response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES); }); +App::patch('/v1/users/:userId/targets/:targetId') + ->desc('Update User target') + ->groups(['api', 'users']) + ->label('audits.event', 'target.update') + ->label('audits.resource', 'target/{response.$id}') + ->label('event', 'users.[userId].targets.[targetId].update') + ->label('scope', 'targets.write') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'updateTarget') + ->label('sdk.description', '/docs/references/users/update-target.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_TARGET) + ->param('userId', '', new UID(), 'User ID.') + ->param('targetId', '', new UID(), 'Target ID.') + ->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)', true) + ->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true) + ->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true) + ->inject('queueForEvents') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $userId, string $targetId, string $identifier, string $providerId, string $name, Event $queueForEvents, Response $response, Database $dbForProject) { + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $target = $dbForProject->getDocument('targets', $targetId); + + if ($target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($user->getId() !== $target->getAttribute('userId')) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($identifier) { + $providerType = $target->getAttribute('providerType'); + + switch ($providerType) { + case 'email': + $validator = new Email(); + if (!$validator->isValid($identifier)) { + throw new Exception(Exception::GENERAL_INVALID_EMAIL); + } + break; + case MESSAGE_TYPE_SMS: + $validator = new Phone(); + if (!$validator->isValid($identifier)) { + throw new Exception(Exception::GENERAL_INVALID_PHONE); + } + break; + case MESSAGE_TYPE_PUSH: + break; + default: + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + $target->setAttribute('identifier', $identifier); + } + + if ($providerId) { + $provider = $dbForProject->getDocument('providers', $providerId); + + if ($provider->isEmpty()) { + throw new Exception(Exception::PROVIDER_NOT_FOUND); + } + + if ($provider->getAttribute('type') !== $target->getAttribute('providerType')) { + throw new Exception(Exception::PROVIDER_INCORRECT_TYPE); + } + + $target->setAttribute('providerId', $provider->getId()); + $target->setAttribute('providerInternalId', $provider->getInternalId()); + } + + if ($name) { + $target->setAttribute('name', $name); + } + + $target = $dbForProject->updateDocument('targets', $target->getId(), $target); + $dbForProject->deleteCachedDocument('users', $user->getId()); + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('targetId', $target->getId()); + + $response + ->dynamic($target, Response::MODEL_TARGET); + }); + App::delete('/v1/users/:userId/sessions/:sessionId') ->desc('Delete user session') ->groups(['api', 'users']) @@ -1210,6 +1550,53 @@ App::delete('/v1/users/:userId') $response->noContent(); }); +App::delete('/v1/users/:userId/targets/:targetId') + ->desc('Delete user target') + ->groups(['api', 'users']) + ->label('audits.event', 'target.delete') + ->label('audits.resource', 'target/{request.$targetId}') + ->label('event', 'users.[userId].targets.[targetId].delete') + ->label('scope', 'targets.write') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'deleteTarget') + ->label('sdk.description', '/docs/references/users/delete-target.md') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_NONE) + ->param('userId', '', new UID(), 'User ID.') + ->param('targetId', '', new UID(), 'Target ID.') + ->inject('queueForEvents') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $userId, string $targetId, Event $queueForEvents, Response $response, Database $dbForProject) { + + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $target = $dbForProject->getDocument('targets', $targetId); + + if ($target->isEmpty()) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + if ($user->getId() !== $target->getAttribute('userId')) { + throw new Exception(Exception::USER_TARGET_NOT_FOUND); + } + + $dbForProject->deleteDocument('targets', $target->getId()); + $dbForProject->deleteCachedDocument('users', $user->getId()); + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('targetId', $target->getId()); + + $response->noContent(); + }); + App::delete('/v1/users/identities/:identityId') ->desc('Delete Identity') ->groups(['api', 'users']) @@ -1251,7 +1638,7 @@ App::get('/v1/users/usage') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USAGE_USERS) ->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true) - ->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn ($value) => "oauth-" . $value, \array_keys(Config::getParam('providers', [])))), true), 'Provider Name.', true) + ->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn ($value) => "oauth-" . $value, \array_keys(Config::getParam('oAuthProviders', [])))), true), 'Provider Name.', true) ->inject('response') ->inject('dbForProject') ->inject('register') diff --git a/app/init.php b/app/init.php index 924122ac20..11cfef20ad 100644 --- a/app/init.php +++ b/app/init.php @@ -25,7 +25,7 @@ use Appwrite\Event\Audit; use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Event; use Appwrite\Event\Mail; -use Appwrite\Event\Phone; +use Appwrite\Event\Messaging; use Appwrite\Event\Delete; use Appwrite\GraphQL\Schema; use Appwrite\Network\Validator\Email; @@ -47,13 +47,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\Structure; use Utopia\Locale\Locale; use Utopia\DSN\DSN; -use Utopia\Messaging\Adapters\SMS\Mock; use Appwrite\GraphQL\Promises\Adapter\Swoole; -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\Registry\Registry; use Utopia\Storage\Device; use Utopia\Storage\Device\Backblaze; @@ -83,6 +77,7 @@ use Utopia\Validator\Range; use Utopia\Validator\IP; use Utopia\Validator\URL; use Utopia\Validator\WhiteList; +use Utopia\CLI\Console; const APP_NAME = 'Appwrite'; const APP_DOMAIN = 'appwrite.io'; @@ -103,6 +98,7 @@ const APP_LIMIT_COMPRESSION = 20000000; //20MB const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements can there be in API parameter that expects array value const APP_LIMIT_ARRAY_ELEMENT_SIZE = 4096; // Default maximum length of element in array parameter represented by maximum URL length. const APP_LIMIT_SUBQUERY = 1000; +const APP_LIMIT_SUBSCRIBERS_SUBQUERY = 1000000; const APP_LIMIT_WRITE_RATE_DEFAULT = 60; // Default maximum write rate per rate period const APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT = 60; // Default maximum write rate period in seconds const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return in list API calls @@ -173,6 +169,7 @@ const DELETE_TYPE_SESSIONS = 'sessions'; const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp'; const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource'; const DELETE_TYPE_SCHEDULES = 'schedules'; +const DELETE_TYPE_TOPIC = 'topic'; // Mail Types const MAIL_TYPE_VERIFICATION = 'verification'; const MAIL_TYPE_MAGIC_SESSION = 'magicSession'; @@ -189,6 +186,10 @@ const MAX_OUTPUT_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB // Function headers const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host']; const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length']; +// Message types +const MESSAGE_TYPE_EMAIL = 'email'; +const MESSAGE_TYPE_SMS = 'sms'; +const MESSAGE_TYPE_PUSH = 'push'; // Usage metrics const METRIC_TEAMS = 'teams'; const METRIC_USERS = 'users'; @@ -233,7 +234,7 @@ App::setMode(App::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION)); Config::load('events', __DIR__ . '/config/events.php'); Config::load('auth', __DIR__ . '/config/auth.php'); Config::load('errors', __DIR__ . '/config/errors.php'); -Config::load('providers', __DIR__ . '/config/providers.php'); +Config::load('oAuthProviders', __DIR__ . '/config/oAuthProviders.php'); Config::load('platforms', __DIR__ . '/config/platforms.php'); Config::load('collections', __DIR__ . '/config/collections.php'); Config::load('runtimes', __DIR__ . '/config/runtimes.php'); @@ -522,6 +523,107 @@ Database::addFilter( } ); +Database::addFilter( + 'subQueryTargets', + function (mixed $value) { + return null; + }, + function (mixed $value, Document $document, Database $database) { + return Authorization::skip(fn() => $database + ->find('targets', [ + Query::equal('userInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBQUERY) + ])); + } +); + +Database::addFilter( + 'subQueryTopicTargets', + function (mixed $value) { + return null; + }, + function (mixed $value, Document $document, Database $database) { + $targetIds = Authorization::skip(fn () => \array_map( + fn ($document) => $document->getAttribute('targetId'), + $database + ->find('subscribers', [ + Query::equal('topicInternalId', [$document->getInternalId()]), + Query::limit(APP_LIMIT_SUBSCRIBERS_SUBQUERY) + ]) + )); + if (\count($targetIds) > 0) { + return $database->find('targets', [Query::equal('$id', $targetIds)]); + } + return []; + } +); + +Database::addFilter( + 'providerSearch', + function (mixed $value, Document $provider) { + $searchValues = [ + $provider->getId(), + $provider->getAttribute('name', ''), + $provider->getAttribute('provider', ''), + $provider->getAttribute('type', '') + ]; + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'topicSearch', + function (mixed $value, Document $topic) { + $searchValues = [ + $topic->getId(), + $topic->getAttribute('name', ''), + $topic->getAttribute('description', ''), + ]; + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + +Database::addFilter( + 'messageSearch', + function (mixed $value, Document $message) { + $searchValues = [ + $message->getId(), + $message->getAttribute('description', ''), + $message->getAttribute('status', ''), + ]; + + $data = \json_decode($message->getAttribute('data', []), true); + $providerType = $message->getAttribute('providerType', ''); + + if ($providerType === MESSAGE_TYPE_EMAIL) { + $searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]); + } elseif ($providerType === MESSAGE_TYPE_SMS) { + $searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]); + } else { + $searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]); + } + + $search = \implode(' ', \array_filter($searchValues)); + + return $search; + }, + function (mixed $value) { + return $value; + } +); + /** * DB Formats */ @@ -902,7 +1004,7 @@ App::setResource('queue', function (Group $pools) { return $pools->get('queue')->pop()->getResource(); }, ['pools']); App::setResource('queueForMessaging', function (Connection $queue) { - return new Phone($queue); + return new Messaging($queue); }, ['queue']); App::setResource('queueForMails', function (Connection $queue) { return new Mail($queue); @@ -1118,7 +1220,7 @@ App::setResource('console', function () { ], 'authWhitelistEmails' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [], 'authWhitelistIPs' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [], - 'authProviders' => [ + 'oAuthProviders' => [ 'githubEnabled' => true, 'githubSecret' => App::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''), 'githubAppid' => App::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '') @@ -1342,21 +1444,6 @@ App::setResource('passwordsDictionary', function ($register) { return $register->get('passwordsDictionary'); }, ['register']); -App::setResource('sms', function () { - $dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER')); - $user = $dsn->getUser(); - $secret = $dsn->getPassword(); - - return match ($dsn->getHost()) { - 'mock' => new Mock($user, $secret), // used for tests - 'twilio' => new Twilio($user, $secret), - 'text-magic' => new TextMagic($user, $secret), - 'telesign' => new Telesign($user, $secret), - 'msg91' => new Msg91($user, $secret), - 'vonage' => new Vonage($user, $secret), - default => null - }; -}); App::setResource('servers', function () { $platforms = Config::getParam('platforms'); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 898b46b3a5..252b5b6bd7 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -521,14 +521,20 @@ services: environment: - _APP_ENV - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER - _APP_REDIS_PASS - - _APP_SMS_PROVIDER - - _APP_SMS_FROM + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG + - _APP_SMS_FROM + - _APP_SMS_PROVIDER appwrite-worker-migrations: image: /: diff --git a/app/worker.php b/app/worker.php index 4f7355311e..a8e5607965 100644 --- a/app/worker.php +++ b/app/worker.php @@ -13,7 +13,6 @@ use Appwrite\Event\Hamster; use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; -use Appwrite\Event\Phone; use Appwrite\Platform\Appwrite; use Appwrite\Usage\Stats; use Swoole\Runtime; @@ -130,7 +129,7 @@ Server::setResource('queueForDatabase', function (Connection $queue) { return new EventDatabase($queue); }, ['queue']); Server::setResource('queueForMessaging', function (Connection $queue) { - return new Phone($queue); + return new Messaging($queue); }, ['queue']); Server::setResource('queueForMails', function (Connection $queue) { return new Mail($queue); diff --git a/docker-compose.yml b/docker-compose.yml index 97cf7e5136..8fca5d67f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -188,7 +188,9 @@ services: - _APP_MIGRATIONS_FIREBASE_CLIENT_ID - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET - _APP_ASSISTANT_OPENAI_API_KEY - + - _APP_MESSAGE_SMS_TEST_DSN + - _APP_MESSAGE_EMAIL_TEST_DSN + - _APP_MESSAGE_PUSH_TEST_DSN appwrite-realtime: entrypoint: realtime <<: *x-logging @@ -568,14 +570,20 @@ services: environment: - _APP_ENV - _APP_WORKER_PER_CORE + - _APP_OPENSSL_KEY_V1 - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER - _APP_REDIS_PASS - - _APP_SMS_PROVIDER - - _APP_SMS_FROM + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG + - _APP_SMS_FROM + - _APP_SMS_PROVIDER appwrite-worker-migrations: entrypoint: worker-migrations diff --git a/docs/tutorials/add-oauth2-provider.md b/docs/tutorials/add-oauth2-provider.md index b3d81d1194..ab33f70cb2 100644 --- a/docs/tutorials/add-oauth2-provider.md +++ b/docs/tutorials/add-oauth2-provider.md @@ -37,7 +37,7 @@ Finally, you will need to create a `feat-XXX-YYY-oauth` branch based on the `mas The first step in adding a new OAuth2 provider is to add it to the list of providers located at: ``` -app/config/providers.php +app/config/oAuthProviders.php ``` Make sure to fill in all data needed and that your provider array key name: @@ -45,7 +45,7 @@ Make sure to fill in all data needed and that your provider array key name: - is in [`camelCase`](https://en.wikipedia.org/wiki/Camel_case) format for sentence, but lowercase for names. `github` must be all lowercased, but `paypalSandbox` should have uppercase S - has no spaces or special characters -> Please make sure to keep the list of providers in `providers.php` in the alphabetical order A-Z. +> Please make sure to keep the list of providers in `oAuthProviders.php` in the alphabetical order A-Z. ### 2.2 Add Provider Logo @@ -199,7 +199,7 @@ If you need any help with the contribution, feel free to head over to [our Disco If your OAuth provider requires special configuration apart from `clientId` and `clientSecret` you can create a custom form. Currently this is being realized through putting all custom fields as JSON into the `clientSecret` field to keep the project API stable. You can implement your custom form following these steps: -1. Add your custom form in `app/views/console/users/oauth/[PROVIDER].phtml`. Below is a template you can use. Add the filename to `app/config/providers.php`. +1. Add your custom form in `app/views/console/users/oauth/[PROVIDER].phtml`. Below is a template you can use. Add the filename to `app/config/oAuthProviders.php`. ```php ./tests/e2e/Services/Projects ./tests/e2e/Services/Storage ./tests/e2e/Services/Webhooks + ./tests/e2e/Services/Messaging ./tests/e2e/Services/Functions/FunctionsBase.php ./tests/e2e/Services/Functions/FunctionsCustomServerTest.php ./tests/e2e/Services/Functions/FunctionsCustomClientTest.php diff --git a/src/Appwrite/Auth/Validator/Password.php b/src/Appwrite/Auth/Validator/Password.php index 93a9f74114..ffb72467e5 100644 --- a/src/Appwrite/Auth/Validator/Password.php +++ b/src/Appwrite/Auth/Validator/Password.php @@ -20,7 +20,7 @@ class Password extends Validator */ public function getDescription(): string { - return 'Password must be at least 8 characters'; + return 'Password must be between 8 and 256 characters long.'; } /** @@ -40,6 +40,10 @@ class Password extends Validator return false; } + if (\strlen($value) > 256) { + return false; + } + return true; } diff --git a/src/Appwrite/Auth/Validator/PasswordDictionary.php b/src/Appwrite/Auth/Validator/PasswordDictionary.php index 003d68bc73..e128f497f5 100644 --- a/src/Appwrite/Auth/Validator/PasswordDictionary.php +++ b/src/Appwrite/Auth/Validator/PasswordDictionary.php @@ -27,7 +27,7 @@ class PasswordDictionary extends Password */ public function getDescription(): string { - return 'Password must be at least 8 characters and should not be one of the commonly used password.'; + return 'Password must be between 8 and 265 characters long, and should not be one of the commonly used password.'; } /** diff --git a/src/Appwrite/Event/Messaging.php b/src/Appwrite/Event/Messaging.php new file mode 100644 index 0000000000..9201799355 --- /dev/null +++ b/src/Appwrite/Event/Messaging.php @@ -0,0 +1,173 @@ +setQueue(Event::MESSAGING_QUEUE_NAME) + ->setClass(Event::MESSAGING_CLASS_NAME); + } + + /** + * Sets recipient for the messaging event. + * + * @param string[] $recipients + * @return self + */ + public function setRecipients(array $recipients): self + { + $this->recipients = $recipients; + + return $this; + } + + /** + * Returns set recipient for messaging event. + * + * @return string[] + */ + public function getRecipient(): array + { + return $this->recipients; + } + + /** + * Sets message document for the messaging event. + * + * @param Document $message + * @return self + */ + public function setMessage(Document $message): self + { + $this->message = $message; + + return $this; + } + + /** + * Returns message document for the messaging event. + * + * @return string + */ + public function getMessage(): Document + { + return $this->message; + } + + /** + * Sets message ID for the messaging event. + * + * @param string $message + * @return self + */ + public function setMessageId(string $messageId): self + { + $this->messageId = $messageId; + + return $this; + } + + /** + * Returns set message ID for the messaging event. + * + * @return string + */ + public function getMessageId(): string + { + return $this->messageId; + } + + /** + * Sets provider type for the messaging event. + * + * @param string $providerType + * @return self + */ + public function setProviderType(string $providerType): self + { + $this->providerType = $providerType; + + return $this; + } + + /** + * Returns set provider type for the messaging event. + * + * @return string + */ + public function getProviderType(): string + { + return $this->providerType; + } + + /** + * Sets Scheduled delivery time for the messaging event. + * + * @param string $scheduledAt + * @return self + */ + public function setScheduledAt(string $scheduledAt): self + { + $this->scheduledAt = $scheduledAt; + + return $this; + } + + /** + * Returns set Delivery Time for the messaging event. + * + * @return string + */ + public function getScheduledAt(): string + { + return $this->scheduledAt; + } + + /** + * Set project for this event. + * + * @param Document $project + * @return self + */ + public function setProject(Document $project): self + { + $this->project = $project; + + return $this; + } + + /** + * Executes the event and sends it to the messaging worker. + * @return string|bool + * @throws \InvalidArgumentException + */ + public function trigger(): string | bool + { + $client = new Client($this->queue, $this->connection); + + return $client->enqueue([ + 'project' => $this->project, + 'user' => $this->user, + 'messageId' => $this->messageId, + 'message' => $this->message, + 'recipients' => $this->recipients, + 'providerType' => $this->providerType, + ]); + } +} diff --git a/src/Appwrite/Event/Phone.php b/src/Appwrite/Event/Phone.php deleted file mode 100644 index 45f193a540..0000000000 --- a/src/Appwrite/Event/Phone.php +++ /dev/null @@ -1,87 +0,0 @@ -setQueue(Event::MESSAGING_QUEUE_NAME) - ->setClass(Event::MESSAGING_CLASS_NAME); - } - - /** - * Sets recipient for the messaging event. - * - * @param string $recipient - * @return self - */ - public function setRecipient(string $recipient): self - { - $this->recipient = $recipient; - - return $this; - } - - /** - * Returns set recipient for this messaging event. - * - * @return string - */ - public function getRecipient(): string - { - return $this->recipient; - } - - /** - * Sets url for the messaging event. - * - * @param string $message - * @return self - */ - public function setMessage(string $message): self - { - $this->message = $message; - - return $this; - } - - /** - * Returns set url for the messaging event. - * - * @return string - */ - public function getMessage(): string - { - return $this->message; - } - - /** - * Executes the event and sends it to the messaging worker. - * - * @return string|bool - * @throws \InvalidArgumentException - */ - public function trigger(): string|bool - { - $client = new Client($this->queue, $this->connection); - - return $client->enqueue([ - 'project' => $this->project, - 'user' => $this->user, - 'payload' => $this->payload, - 'recipient' => $this->recipient, - 'message' => $this->message, - 'events' => Event::generateEvents($this->getEvent(), $this->getParams()) - ]); - } -} diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 6449ffd93a..ea63423c05 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -55,6 +55,8 @@ class Exception extends \Exception public const GENERAL_CODES_DISABLED = 'general_codes_disabled'; public const GENERAL_USAGE_DISABLED = 'general_usage_disabled'; public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented'; + public const GENERAL_INVALID_EMAIL = 'general_invalid_email'; + public const GENERAL_INVALID_PHONE = 'general_invalid_phone'; /** Users */ public const USER_COUNT_EXCEEDED = 'user_count_exceeded'; @@ -86,6 +88,8 @@ class Exception extends \Exception public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error'; public const USER_EMAIL_ALREADY_VERIFIED = 'user_email_alread_verified'; public const USER_PHONE_ALREADY_VERIFIED = 'user_phone_already_verified'; + public const USER_TARGET_NOT_FOUND = 'user_target_not_found'; + public const USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists'; /** Teams */ public const TEAM_NOT_FOUND = 'team_not_found'; @@ -236,6 +240,30 @@ class Exception extends \Exception /** Health */ public const QUEUE_SIZE_EXCEEDED = 'queue_size_exceeded'; + /** Provider */ + 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_INTERNAL_UPDATE_DISABLED = 'provider_internal_update_disabled'; + + /** Topic */ + public const TOPIC_NOT_FOUND = 'topic_not_found'; + public const TOPIC_ALREADY_EXISTS = 'topic_already_exists'; + + /** Subscriber */ + public const SUBSCRIBER_NOT_FOUND = 'subscriber_not_found'; + public const SUBSCRIBER_ALREADY_EXISTS = 'subscriber_already_exists'; + + /** Message */ + public const MESSAGE_NOT_FOUND = 'message_not_found'; + public const MESSAGE_MISSING_TARGET = 'message_missing_target'; + public const MESSAGE_ALREADY_SENT = 'message_already_sent'; + public const MESSAGE_ALREADY_SCHEDULED = 'message_already_scheduled'; + public const MESSAGE_TARGET_NOT_EMAIL = 'message_target_not_email'; + public const MESSAGE_TARGET_NOT_SMS = 'message_target_not_sms'; + public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push'; + + protected string $type = ''; protected array $errors = []; protected bool $publish = true; diff --git a/src/Appwrite/Migration/Version/V15.php b/src/Appwrite/Migration/Version/V15.php index 60f5fa20ab..1a8e8a4265 100644 --- a/src/Appwrite/Migration/Version/V15.php +++ b/src/Appwrite/Migration/Version/V15.php @@ -34,7 +34,7 @@ class V15 extends Migration ['email', 'anonymous'], \array_map( fn ($value) => "oauth-" . $value, - \array_keys(Config::getParam('providers', [])) + \array_keys(Config::getParam('oAuthProviders', [])) ) ); diff --git a/src/Appwrite/Migration/Version/V16.php b/src/Appwrite/Migration/Version/V16.php index 1d56b246d6..49f244598e 100644 --- a/src/Appwrite/Migration/Version/V16.php +++ b/src/Appwrite/Migration/Version/V16.php @@ -124,23 +124,23 @@ class V16 extends Migration /** * Enable OAuth providers with data */ - $authProviders = $document->getAttribute('authProviders', []); + $oAuthProviders = $document->getAttribute('oAuthProviders', []); - foreach (Config::getParam('providers') as $provider => $value) { + foreach (Config::getParam('oAuthProviders') as $provider => $value) { if (!$value['enabled']) { continue; } - if (($authProviders[$provider . 'Appid'] ?? false) && ($authProviders[$provider . 'Secret'] ?? false)) { - if (array_key_exists($provider . 'Enabled', $authProviders)) { + if (($oAuthProviders[$provider . 'Appid'] ?? false) && ($oAuthProviders[$provider . 'Secret'] ?? false)) { + if (array_key_exists($provider . 'Enabled', $oAuthProviders)) { continue; } - $authProviders[$provider . 'Enabled'] = true; + $oAuthProviders[$provider . 'Enabled'] = true; } } - $document->setAttribute('authProviders', $authProviders); + $document->setAttribute('oAuthProviders', $oAuthProviders); break; } diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index b95a13a12e..9a9e965f37 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Workers; +use Appwrite\Auth\Auth; use Executor\Executor; use Throwable; use Utopia\Abuse\Abuse; @@ -149,6 +150,9 @@ class Deletes extends Action case DELETE_TYPE_SCHEDULES: $this->deleteSchedules($dbForConsole, $getProjectDB, $datetime); break; + case DELETE_TYPE_TOPIC: + $this->deleteTopic($project, $getProjectDB, $document); + break; default: Console::error('No delete operation for type: ' . $type); break; @@ -193,6 +197,25 @@ class Deletes extends Action ); } + /** + * @param Document $project + * @param callable $getProjectDB + * @param Document $topic + * @throws Exception + */ + protected function deleteTopic(Document $project, callable $getProjectDB, Document $topic) + { + if ($topic->isEmpty()) { + Console::error('Failed to delete subscribers. Topic not found'); + return; + } + $dbForProject = $getProjectDB($project); + + $this->deleteByGroup('subscribers', [ + Query::equal('topicInternalId', [$topic->getInternalId()]) + ], $dbForProject); + } + /** * @param Document $project * @param callable $getProjectDB @@ -533,6 +556,11 @@ class Deletes extends Action $this->deleteByGroup('identities', [ Query::equal('userInternalId', [$userInternalId]) ], $dbForProject); + + // Delete targets + $this->deleteByGroup('targets', [ + Query::equal('userInternalId', [$userInternalId]) + ], $dbForProject); } /** diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 876ca50d0d..8f945e947f 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -2,30 +2,41 @@ namespace Appwrite\Platform\Workers; -use Exception; +use Appwrite\Extend\Exception; use Utopia\App; use Utopia\CLI\Console; +use Utopia\Database\Helpers\ID; use Utopia\DSN\DSN; -use Utopia\Messaging\Messages\SMS; +use Utopia\Platform\Action; +use Utopia\Queue\Message; +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\Textmagic; use Utopia\Messaging\Adapters\SMS\Twilio; use Utopia\Messaging\Adapters\SMS\Vonage; -use Utopia\Platform\Action; -use Utopia\Queue\Message; +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\Messages\Email; +use Utopia\Messaging\Messages\Push; +use Utopia\Messaging\Messages\SMS; + +use function Swoole\Coroutine\batch; class Messaging extends Action { - private ?DSN $dsn = null; - private string $user = ''; - private string $secret = ''; - private string $provider = ''; - public static function getName(): string { - return 'messaging'; + return "messaging"; } /** @@ -33,25 +44,20 @@ class Messaging extends Action */ public function __construct() { - $this->provider = App::getEnv('_APP_SMS_PROVIDER', ''); - if (!empty($this->provider)) { - $this->dsn = new DSN($this->provider); - $this->user = $this->dsn->getUser(); - $this->secret = $this->dsn->getPassword(); - } - $this ->desc('Messaging worker') ->inject('message') - ->callback(fn($message) => $this->action($message)); + ->inject('dbForProject') + ->callback(fn(Message $message, Database $dbForProject) => $this->action($message, $dbForProject)); } /** * @param Message $message + * @param Database $dbForProject * @return void * @throws Exception */ - public function action(Message $message): void + public function action(Message $message, Database $dbForProject): void { $payload = $message->getPayload() ?? []; @@ -60,48 +66,318 @@ class Messaging extends Action return; } - if (empty($payload['recipient'])) { - Console::error('Recipient arg not found'); + if (!\is_null($payload['message']) && !\is_null($payload['recipients'])) { + if ($payload['providerType'] === MESSAGE_TYPE_SMS) { + $this->processInternalSMSMessage(new Document($payload['message']), $payload['recipients']); + } + } else { + $message = $dbForProject->getDocument('messages', $payload['messageId']); + + $this->processMessage($dbForProject, $message); + } + } + + private function processMessage(Database $dbForProject, Document $message): void + { + $topicsId = $message->getAttribute('topics', []); + $targetsId = $message->getAttribute('targets', []); + $usersId = $message->getAttribute('users', []); + + /** + * @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')); + $recipients = \array_merge($recipients, $targets); + } + } + + 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')); + $recipients = \array_merge($recipients, $targets); + } + } + + if (\count($targetsId) > 0) { + $targets = $dbForProject->find('targets', [Query::equal('$id', $targetsId)]); + $recipients = \array_merge($recipients, $targets); + } + + $primaryProvider = $dbForProject->findOne('providers', [ + Query::equal('enabled', [true]), + Query::equal('type', [$recipients[0]->getAttribute('providerType')]), + ]); + + /** + * @var array> $identifiersByProviderId + */ + $identifiersByProviderId = []; + + /** + * @var Document[] $providers + */ + $providers = []; + foreach ($recipients as $recipient) { + $providerId = $recipient->getAttribute('providerId'); + + if (!$providerId && $primaryProvider instanceof Document && !$primaryProvider->isEmpty()) { + $providerId = $primaryProvider->getId(); + } + + if ($providerId) { + if (!isset($identifiersByProviderId[$providerId])) { + $identifiersByProviderId[$providerId] = []; + } + $identifiersByProviderId[$providerId][] = $recipient->getAttribute('identifier'); + } + } + + /** + * @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; + } else { + $provider = $dbForProject->getDocument('providers', $providerId, [Query::equal('enabled', [true])]); + + if ($provider->isEmpty()) { + $provider = $primaryProvider; + } + } + + $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), + default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) + }; + + $maxBatchSize = $adapter->getMaxMessagesPerRequest(); + $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) { + $deliveredTotal = 0; + $deliveryErrors = []; + $messageData = clone $message; + $messageData->setAttribute('to', $batch); + + $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), + default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) + }; + + try { + $adapter->send($data); + $deliveredTotal += \count($batch); + } catch (\Exception $e) { + $deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage(); + } finally { + $batchIndex++; + return [ + 'deliveredTotal' => $deliveredTotal, + 'deliveryErrors' => $deliveryErrors, + ]; + } + }; + }, $batches)); + + return $results; + }; + }, \array_keys($identifiersByProviderId))); + + $results = array_merge(...$results); + + $deliveredTotal = 0; + $deliveryErrors = []; + + foreach ($results as $result) { + $deliveredTotal += $result['deliveredTotal']; + $deliveryErrors = \array_merge($deliveryErrors, $result['deliveryErrors']); + } + + $message->setAttribute('deliveryErrors', $deliveryErrors); + + if (\count($message->getAttribute('deliveryErrors')) > 0) { + $message->setAttribute('status', 'failed'); + } else { + $message->setAttribute('status', 'sent'); + } + + $message->removeAttribute('to'); + + foreach ($providers as $provider) { + $message->setAttribute('search', "{$message->getAttribute('search')} {$provider->getAttribute('name')} {$provider->getAttribute('provider')} {$provider->getAttribute('type')}"); + } + + $message->setAttribute('deliveredTotal', $deliveredTotal); + $message->setAttribute('deliveredAt', DateTime::now()); + + $dbForProject->updateDocument('messages', $message->getId(), $message); + } + + private function processInternalSMSMessage(Document $message, array $recipients): void + { + if (empty(App::getEnv('_APP_SMS_PROVIDER')) || empty(App::getEnv('_APP_SMS_FROM'))) { + Console::info('Skipped SMS processing. No Phone configuration has been set.'); return; } - if (empty($payload['message'])) { - Console::error('Message arg not found'); - return; - } - - $sms = match ($this->dsn->getHost()) { - 'mock' => new Mock($this->user, $this->secret), // used for tests - 'twilio' => new Twilio($this->user, $this->secret), - 'text-magic' => new TextMagic($this->user, $this->secret), - 'telesign' => new Telesign($this->user, $this->secret), - 'msg91' => new Msg91($this->user, $this->secret), - 'vonage' => new Vonage($this->user, $this->secret), - default => null - }; - - if (empty(App::getEnv('_APP_SMS_PROVIDER'))) { - Console::error('Skipped sms processing. No Phone provider has been set.'); - return; - } + $smsDSN = new DSN(App::getEnv('_APP_SMS_PROVIDER')); + $host = $smsDSN->getHost(); + $password = $smsDSN->getPassword(); + $user = $smsDSN->getUser(); $from = App::getEnv('_APP_SMS_FROM'); - if (empty($from)) { - Console::error('Skipped sms processing. No phone number has been set.'); - return; - } + $provider = new Document([ + '$id' => ID::unique(), + 'provider' => $host, + 'type' => MESSAGE_TYPE_SMS, + 'name' => 'Internal SMS', + 'enabled' => true, + 'credentials' => match ($host) { + 'twilio' => [ + 'accountSid' => $user, + 'authToken' => $password + ], + 'textmagic' => [ + 'username' => $user, + 'apiKey' => $password + ], + 'telesign' => [ + 'username' => $user, + 'password' => $password + ], + 'msg91' => [ + 'senderId' => $user, + 'authKey' => $password + ], + 'vonage' => [ + 'apiKey' => $user, + 'apiSecret' => $password + ], + default => null + }, + 'options' => [ + 'from' => $from + ] + ]); - $message = new SMS( - to: [$payload['recipient']], - content: $payload['message'], - from: $from, - ); + $adapter = $this->sms($provider); - try { - $sms->send($message); - } catch (\Exception $error) { - throw new Exception('Error sending message: ' . $error->getMessage(), 500); - } + $maxBatchSize = $adapter->getMaxMessagesPerRequest(); + $batches = \array_chunk($recipients, $maxBatchSize); + $batchIndex = 0; + + batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) { + return function () use ($batch, $message, $provider, $adapter, $batchIndex) { + $message->setAttribute('to', $batch); + + $data = $this->buildSMSMessage($message, $provider); + + try { + $adapter->send($data); + } catch (\Exception $e) { + Console::error('Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage()); + } + }; + }, $batches)); + } + + public function shutdown(): void + { + } + + private function sms(Document $provider): ?SMSAdapter + { + $credentials = $provider->getAttribute('credentials'); + return match ($provider->getAttribute('provider')) { + 'mock' => new Mock('username', 'password'), + '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']), + 'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']), + default => null + }; + } + + private function push(Document $provider): ?PushAdapter + { + $credentials = $provider->getAttribute('credentials'); + return match ($provider->getAttribute('provider')) { + 'apns' => new APNS( + $credentials['authKey'], + $credentials['authKeyId'], + $credentials['teamId'], + $credentials['bundleId'], + $credentials['endpoint'] + ), + 'fcm' => new FCM($credentials['serverKey']), + default => null + }; + } + + private function email(Document $provider): ?EmailAdapter + { + $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 + }; + } + + private function buildEmailMessage(Document $message, Document $provider): Email + { + $from = $provider['options']['from']; + $to = $message['to']; + $subject = $message['data']['subject']; + $content = $message['data']['content']; + $html = $message['data']['html']; + + return new Email($to, $subject, $content, $from, null, $html); + } + + private function buildSMSMessage(Document $message, Document $provider): SMS + { + $to = $message['to']; + $content = $message['data']['content']; + $from = $provider['options']['from']; + + return new SMS($to, $content, $from); + } + + private function buildPushMessage(Document $message): Push + { + $to = $message['to']; + $title = $message['data']['title']; + $body = $message['data']['body']; + $data = $message['data']['data']; + $action = $message['data']['action']; + $sound = $message['data']['sound']; + $icon = $message['data']['icon']; + $color = $message['data']['color']; + $tag = $message['data']['tag']; + $badge = $message['data']['badge']; + + return new Push($to, $title, $body, $data, $action, $sound, $icon, $color, $tag, $badge); } } diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Messages.php b/src/Appwrite/Utopia/Database/Validator/Queries/Messages.php new file mode 100644 index 0000000000..dd043474a8 --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Messages.php @@ -0,0 +1,28 @@ +setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false)) ->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false)) ->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false)) - ->setModel(new BaseList('Providers List', self::MODEL_PROVIDER_LIST, 'platforms', self::MODEL_PROVIDER, true, false)) + ->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false)) ->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false)) ->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY)) ->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT)) @@ -328,6 +345,11 @@ class Response extends SwooleResponse ->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS)) ->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE)) ->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE)) + ->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER)) + ->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE)) + ->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC)) + ->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER)) + ->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET)) ->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION)) ->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT)) // Entities @@ -380,7 +402,7 @@ class Response extends SwooleResponse ->setModel(new Project()) ->setModel(new Webhook()) ->setModel(new Key()) - ->setModel(new Provider()) + ->setModel(new AuthProvider()) ->setModel(new Platform()) ->setModel(new Variable()) ->setModel(new Country()) @@ -408,6 +430,11 @@ class Response extends SwooleResponse ->setModel(new TemplateSMS()) ->setModel(new TemplateEmail()) ->setModel(new ConsoleVariables()) + ->setModel(new Provider()) + ->setModel(new Message()) + ->setModel(new Topic()) + ->setModel(new Subscriber()) + ->setModel(new Target()) ->setModel(new Migration()) ->setModel(new MigrationReport()) ->setModel(new MigrationFirebaseProject()) diff --git a/src/Appwrite/Utopia/Response/Filters/V13.php b/src/Appwrite/Utopia/Response/Filters/V13.php index d48473593e..04f9782915 100644 --- a/src/Appwrite/Utopia/Response/Filters/V13.php +++ b/src/Appwrite/Utopia/Response/Filters/V13.php @@ -60,8 +60,8 @@ class V13 extends Filter protected function parseProject($content) { - $content['providers'] = $content['authProviders']; - unset($content['authProviders']); + $content['providers'] = $content['oAuthProviders']; + unset($content['oAuthProviders']); return $content; } diff --git a/src/Appwrite/Utopia/Response/Filters/V16.php b/src/Appwrite/Utopia/Response/Filters/V16.php index 6943bd8f4d..609f118f6e 100644 --- a/src/Appwrite/Utopia/Response/Filters/V16.php +++ b/src/Appwrite/Utopia/Response/Filters/V16.php @@ -88,9 +88,9 @@ class V16 extends Filter protected function parseProject(array $content) { - foreach ($content['providers'] ?? [] as $i => $provider) { - $content['providers'][$i]['name'] = \ucfirst($provider['key']); - unset($content['providers'][$i]['key']); + foreach ($content['oAuthProviders'] ?? [] as $i => $provider) { + $content['oAuthProviders'][$i]['name'] = \ucfirst($provider['key']); + unset($content['oAuthProviders'][$i]['key']); } $content['domains'] = []; diff --git a/src/Appwrite/Utopia/Response/Model/AuthProvider.php b/src/Appwrite/Utopia/Response/Model/AuthProvider.php new file mode 100644 index 0000000000..0171a3c152 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/AuthProvider.php @@ -0,0 +1,69 @@ +addRule('key', [ + 'type' => self::TYPE_STRING, + 'description' => 'Auth Provider.', + 'default' => '', + 'example' => 'github', + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'Auth Provider name.', + 'default' => '', + 'example' => 'GitHub', + ]) + ->addRule('appId', [ + 'type' => self::TYPE_STRING, + 'description' => 'OAuth 2.0 application ID.', + 'default' => '', + 'example' => '259125845563242502', + ]) + ->addRule('secret', [ + 'type' => self::TYPE_STRING, + 'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration.', + 'default' => '', + 'example' => 'Bpw_g9c2TGXxfgLshDbSaL8tsCcqgczQ', + ]) + ->addRule('enabled', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Auth Provider is active and can be used to create session.', + 'example' => '', + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'AuthProvider'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_AUTH_PROVIDER; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Message.php b/src/Appwrite/Utopia/Response/Model/Message.php new file mode 100644 index 0000000000..791c87933f --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Message.php @@ -0,0 +1,131 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Message ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('$createdAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Message creation time in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('$updatedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Message update date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('providerType', [ + 'type' => self::TYPE_STRING, + 'description' => 'Message provider type.', + 'default' => '', + 'example' => MESSAGE_TYPE_EMAIL, + ]) + ->addRule('topics', [ + 'type' => self::TYPE_STRING, + 'description' => 'Topic IDs set as recipients.', + 'default' => '', + 'array' => true, + 'example' => ['5e5ea5c16897e'], + ]) + ->addRule('users', [ + 'type' => self::TYPE_STRING, + 'description' => 'User IDs set as recipients.', + 'default' => '', + 'array' => true, + 'example' => ['5e5ea5c16897e'], + ]) + ->addRule('targets', [ + 'type' => self::TYPE_STRING, + 'description' => 'Target IDs set as recipients.', + 'default' => '', + 'array' => true, + 'example' => ['5e5ea5c16897e'], + ]) + ->addRule('scheduledAt', [ + 'type' => self::TYPE_DATETIME, + '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.', + 'required' => false, + 'default' => '', + 'array' => true, + 'example' => ['Failed to send message to target 5e5ea5c16897e: Credentials not valid.'], + ]) + ->addRule('deliveredTotal', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Number of recipients the message was delivered to.', + 'default' => 0, + 'example' => 1, + ]) + ->addRule('data', [ + 'type' => self::TYPE_JSON, + 'description' => 'Data of the message.', + 'default' => [], + 'example' => [ + 'subject' => 'Welcome to Appwrite', + 'content' => 'Hi there, welcome to Appwrite family.', + ], + ]) + ->addRule('status', [ + 'type' => self::TYPE_STRING, + 'description' => 'Status of delivery.', + 'default' => 'processing', + 'example' => 'Message status can be one of the following: processing, sent, cancelled, failed.', + ]) + ->addRule('description', [ + 'type' => self::TYPE_STRING, + 'description' => 'Message description.', + 'required' => false, + 'default' => '', + 'example' => 'Welcome Email.', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Message'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_MESSAGE; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 20703ffbeb..6bab4401d7 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -138,9 +138,9 @@ class Project extends Model 'default' => false, 'example' => true, ]) - ->addRule('providers', [ - 'type' => Response::MODEL_PROVIDER, - 'description' => 'List of Providers.', + ->addRule('oAuthProviders', [ + 'type' => Response::MODEL_AUTH_PROVIDER, + 'description' => 'List of Auth Providers.', 'default' => [], 'example' => [new \stdClass()], 'array' => true, @@ -328,9 +328,9 @@ class Project extends Model $document->setAttribute('auth' . ucfirst($key), $value); } - // Providers - $providers = Config::getParam('providers', []); - $providerValues = $document->getAttribute('authProviders', []); + // OAuth Providers + $providers = Config::getParam('oAuthProviders', []); + $providerValues = $document->getAttribute('oAuthProviders', []); $projectProviders = []; foreach ($providers as $key => $provider) { @@ -348,7 +348,7 @@ class Project extends Model ]); } - $document->setAttribute("providers", $projectProviders); + $document->setAttribute('oAuthProviders', $projectProviders); return $document; } diff --git a/src/Appwrite/Utopia/Response/Model/Provider.php b/src/Appwrite/Utopia/Response/Model/Provider.php index c589011a46..d3de061aab 100644 --- a/src/Appwrite/Utopia/Response/Model/Provider.php +++ b/src/Appwrite/Utopia/Response/Model/Provider.php @@ -7,44 +7,68 @@ use Appwrite\Utopia\Response\Model; class Provider extends Model { - /** - * @var bool - */ - protected bool $public = false; - public function __construct() { $this - ->addRule('key', [ + ->addRule('$id', [ 'type' => self::TYPE_STRING, - 'description' => 'Provider.', + 'description' => 'Provider ID.', 'default' => '', - 'example' => 'github', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('$createdAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Provider creation time in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('$updatedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Provider update date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, ]) ->addRule('name', [ 'type' => self::TYPE_STRING, - 'description' => 'Provider name.', + 'description' => 'The name for the provider instance.', 'default' => '', - 'example' => 'GitHub', + 'example' => 'Mailgun', ]) - ->addRule('appId', [ + ->addRule('provider', [ 'type' => self::TYPE_STRING, - 'description' => 'OAuth 2.0 application ID.', + 'description' => 'The name of the provider service.', 'default' => '', - 'example' => '259125845563242502', - ]) - ->addRule('secret', [ - 'type' => self::TYPE_STRING, - 'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration.', - 'default' => '', - 'example' => 'Bpw_g9c2TGXxfgLshDbSaL8tsCcqgczQ', + 'example' => 'mailgun', ]) ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, - 'description' => 'Provider is active and can be used to create session.', - 'example' => '', + 'description' => 'Is provider enabled?', + 'default' => true, + 'example' => true, ]) - ; + ->addRule('type', [ + 'type' => self::TYPE_STRING, + 'description' => 'Type of provider.', + 'default' => '', + 'example' => MESSAGE_TYPE_SMS, + ]) + ->addRule('credentials', [ + 'type' => self::TYPE_JSON, + 'description' => 'Provider credentials.', + 'default' => [], + 'example' => [ + 'key' => '123456789' + ], + ]) + ->addRule('options', [ + 'type' => self::TYPE_JSON, + 'description' => 'Provider options.', + 'default' => [], + 'required' => false, + 'example' => [ + 'from' => 'sender-email@mydomain' + ], + ]); } /** diff --git a/src/Appwrite/Utopia/Response/Model/Subscriber.php b/src/Appwrite/Utopia/Response/Model/Subscriber.php new file mode 100644 index 0000000000..8c3a4c7a49 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Subscriber.php @@ -0,0 +1,97 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Subscriber ID.', + 'default' => '', + 'example' => '259125845563242502', + ]) + ->addRule('$createdAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Subscriber creation time in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('$updatedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Subscriber update date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('targetId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Target ID.', + 'default' => '', + 'example' => '259125845563242502', + ]) + ->addRule('target', [ + 'type' => Response::MODEL_TARGET, + 'description' => 'Target.', + 'default' => [], + 'example' => [ + '$id' => '259125845563242502', + '$createdAt' => self::TYPE_DATETIME_EXAMPLE, + '$updatedAt' => self::TYPE_DATETIME_EXAMPLE, + 'providerType' => 'email', + 'providerId' => '259125845563242502', + 'name' => 'ageon-app-email', + 'identifier' => 'random-mail@email.org', + 'userId' => '5e5ea5c16897e', + ], + ]) + ->addRule('userId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Topic ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('userName', [ + 'type' => self::TYPE_STRING, + 'description' => 'User Name.', + 'default' => '', + 'example' => 'Aegon Targaryen', + ]) + ->addRule('topicId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Topic ID.', + 'default' => '', + 'example' => '259125845563242502', + ]) + ->addRule('providerType', [ + 'type' => self::TYPE_STRING, + 'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.', + 'default' => '', + 'example' => MESSAGE_TYPE_EMAIL, + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Subscriber'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_SUBSCRIBER; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Target.php b/src/Appwrite/Utopia/Response/Model/Target.php new file mode 100644 index 0000000000..d180b6c4c4 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Target.php @@ -0,0 +1,83 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Target ID.', + 'default' => '', + 'example' => '259125845563242502', + ]) + ->addRule('$createdAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Target creation time in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('$updatedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Target update date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'Target Name.', + 'default' => '', + 'example' => 'Aegon apple token', + ]) + ->addRule('userId', [ + 'type' => self::TYPE_STRING, + 'description' => 'User ID.', + 'default' => '', + 'example' => '259125845563242502', + ]) + ->addRule('providerId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Provider ID.', + 'required' => false, + 'default' => '', + 'example' => '259125845563242502', + ]) + ->addRule('providerType', [ + 'type' => self::TYPE_STRING, + 'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.', + 'default' => '', + 'example' => MESSAGE_TYPE_EMAIL, + ]) + ->addRule('identifier', [ + 'type' => self::TYPE_STRING, + 'description' => 'The target identifier.', + 'default' => '', + 'example' => 'token', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Target'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_TARGET; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Topic.php b/src/Appwrite/Utopia/Response/Model/Topic.php new file mode 100644 index 0000000000..096ddb347f --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Topic.php @@ -0,0 +1,71 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Topic ID.', + 'default' => '', + 'example' => '259125845563242502', + ]) + ->addRule('$createdAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Topic creation time in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('$updatedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Topic update date in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'The name of the topic.', + 'default' => '', + 'example' => 'events', + ]) + ->addRule('total', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total count of subscribers subscribed to topic.', + 'default' => 0, + 'example' => 100, + ]) + ->addRule('description', [ + 'type' => self::TYPE_STRING, + 'description' => 'Description of the topic.', + 'default' => '', + 'required' => false, + 'example' => 'All events related messages will be sent to this topic.', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Topic'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_TOPIC; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/User.php b/src/Appwrite/Utopia/Response/Model/User.php index ce2bd6188c..d6988b47f4 100644 --- a/src/Appwrite/Utopia/Response/Model/User.php +++ b/src/Appwrite/Utopia/Response/Model/User.php @@ -120,6 +120,13 @@ class User extends Model 'default' => new \stdClass(), 'example' => ['theme' => 'pink', 'timezone' => 'UTC'], ]) + ->addRule('targets', [ + 'type' => Response::MODEL_TARGET, + 'description' => 'A user-owned message receiver. A single user may have multiple e.g. emails, phones, and a browser. Each target is registered with a single provider.', + 'default' => [], + 'array' => true, + 'example' => [], + ]) ->addRule('accessedAt', [ 'type' => self::TYPE_DATETIME, 'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_USER_ACCCESS / 60 / 60 . ' hours.', diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 5f7bf85d0e..a84a91893c 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -82,7 +82,17 @@ trait ProjectCustom 'avatars.read', 'health.read', 'rules.read', - 'rules.write' + 'rules.write', + 'targets.read', + 'targets.write', + 'providers.read', + 'providers.write', + 'messages.read', + 'messages.write', + 'topics.write', + 'topics.read', + 'subscribers.write', + 'subscribers.read', ], ]); diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index e6f5feaa84..fe9983d9b8 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -94,6 +94,36 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 400); + $shortPassword = 'short'; + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => ID::unique(), + 'email' => 'shortpass@appwrite.io', + 'password' => $shortPassword + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $longPassword = ''; + for ($i = 0; $i < 257; $i++) { // 256 is the limit + $longPassword .= 'p'; + } + + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'userId' => ID::unique(), + 'email' => 'longpass@appwrite.io', + 'password' => $longPassword, + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + return [ 'id' => $id, 'email' => $email, diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 56280b4b49..721a16b2b2 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -2,16 +2,16 @@ namespace Tests\E2E\Services\Account; -use Appwrite\Extend\Exception; -use Appwrite\SMS\Adapter\Mock; use Appwrite\Tests\Retry; use Tests\E2E\Client; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\SideClient; +use Utopia\App; use Utopia\Database\DateTime; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\DSN\DSN; use function sleep; @@ -764,6 +764,7 @@ class AccountCustomClientTest extends Scope $this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire'])); $userId = $response['body']['userId']; + $messageId = $response['body']['$id']; /** * Test for FAILURE diff --git a/tests/e2e/Services/GraphQL/AccountTest.php b/tests/e2e/Services/GraphQL/AccountTest.php index 3985988d78..970d009bac 100644 --- a/tests/e2e/Services/GraphQL/AccountTest.php +++ b/tests/e2e/Services/GraphQL/AccountTest.php @@ -6,7 +6,9 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; +use Utopia\App; use Utopia\Database\Helpers\ID; +use Utopia\DSN\DSN; class AccountTest extends Scope { @@ -123,6 +125,7 @@ class AccountTest extends Scope public function testCreatePhoneVerification(): array { $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::$CREATE_PHONE_VERIFICATION); $graphQLPayload = [ 'query' => $query, diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index f705e8d77b..2854d0bf42 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -109,6 +109,11 @@ trait Base public static string $DELETE_USER_SESSIONS = 'delete_user_sessions'; public static string $DELETE_USER_SESSION = 'delete_user_session'; public static string $DELETE_USER = 'delete_user'; + public static string $CREATE_USER_TARGET = 'create_user_target'; + public static string $LIST_USER_TARGETS = 'list_user_targets'; + public static string $GET_USER_TARGET = 'get_user_target'; + public static string $UPDATE_USER_TARGET = 'update_user_target'; + public static string $DELETE_USER_TARGET = 'delete_user_target'; // Teams public static string $GET_TEAM = 'get_team'; @@ -199,6 +204,53 @@ trait Base public static string $GET_QRCODE = 'get_qrcode'; public static string $GET_USER_INITIALS = 'get_user_initials'; + // Providers + public static string $CREATE_MAILGUN_PROVIDER = 'create_mailgun_provider'; + public static string $CREATE_SENDGRID_PROVIDER = 'create_sendgrid_provider'; + public static string $CREATE_TWILIO_PROVIDER = 'create_twilio_provider'; + public static string $CREATE_TELESIGN_PROVIDER = 'create_telesign_provider'; + public static string $CREATE_TEXTMAGIC_PROVIDER = 'create_textmagic_provider'; + public static string $CREATE_MSG91_PROVIDER = 'create_msg91_provider'; + public static string $CREATE_VONAGE_PROVIDER = 'create_vonage_provider'; + public static string $CREATE_FCM_PROVIDER = 'create_fcm_provider'; + public static string $CREATE_APNS_PROVIDER = 'create_apns_provider'; + public static string $LIST_PROVIDERS = 'list_providers'; + public static string $GET_PROVIDER = 'get_provider'; + public static string $UPDATE_MAILGUN_PROVIDER = 'update_mailgun_provider'; + public static string $UPDATE_SENDGRID_PROVIDER = 'update_sendgrid_provider'; + public static string $UPDATE_TWILIO_PROVIDER = 'update_twilio_provider'; + public static string $UPDATE_TELESIGN_PROVIDER = 'update_telesign_provider'; + public static string $UPDATE_TEXTMAGIC_PROVIDER = 'update_textmagic_provider'; + public static string $UPDATE_MSG91_PROVIDER = 'update_msg91_provider'; + public static string $UPDATE_VONAGE_PROVIDER = 'update_vonage_provider'; + public static string $UPDATE_FCM_PROVIDER = 'update_fcm_provider'; + public static string $UPDATE_APNS_PROVIDER = 'update_apns_provider'; + public static string $DELETE_PROVIDER = 'delete_provider'; + + // Topics + public static string $CREATE_TOPIC = 'create_topic'; + public static string $LIST_TOPICS = 'list_topics'; + public static string $GET_TOPIC = 'get_topic'; + public static string $UPDATE_TOPIC = 'update_topic'; + public static string $DELETE_TOPIC = 'delete_topic'; + + // Subscriptions + public static string $CREATE_SUBSCRIBER = 'create_subscriber'; + public static string $LIST_SUBSCRIBERS = 'list_subscribers'; + public static string $GET_SUBSCRIBER = 'get_subscriber'; + public static string $DELETE_SUBSCRIBER = 'delete_subscriber'; + + // Messages + public static string $CREATE_EMAIL = 'create_email'; + public static string $CREATE_SMS = 'create_sms'; + public static string $CREATE_PUSH_NOTIFICATION = 'create_push_notification'; + public static string $LIST_MESSAGES = 'list_messages'; + public static string $GET_MESSAGE = 'get_message'; + + public static string $UPDATE_EMAIL = 'update_email'; + public static string $UPDATE_SMS = 'update_sms'; + public static string $UPDATE_PUSH_NOTIFICATION = 'update_push_notification'; + // Complex queries public static string $COMPLEX_QUERY = 'complex_query'; @@ -881,6 +933,55 @@ trait Base status } }'; + case self::$CREATE_USER_TARGET: + return 'mutation createUserTarget($userId: String!, $targetId: String!, $providerType: String!, $identifier: String! $providerId: String){ + usersCreateTarget(userId: $userId, targetId: $targetId, providerType: $providerType, identifier: $identifier, providerId: $providerId) { + _id + userId + providerType + providerId + identifier + } + }'; + case self::$LIST_USER_TARGETS: + return 'query listUserTargets($userId: String!) { + usersListTargets(userId: $userId) { + total + targets { + _id + userId + providerType + providerId + identifier + } + } + }'; + case self::$GET_USER_TARGET: + return 'query getUserTarget($userId: String!, $targetId: String!) { + usersGetTarget(userId: $userId, targetId: $targetId) { + _id + userId + providerType + providerId + identifier + } + }'; + case self::$UPDATE_USER_TARGET: + return 'mutation updateUserTarget($userId: String!, $targetId: String!, $providerId: String, $identifier: String){ + usersUpdateTarget(userId: $userId, targetId: $targetId, providerId: $providerId, identifier: $identifier) { + _id + userId + providerType + providerId + identifier + } + }'; + case self::$DELETE_USER_TARGET: + return 'mutation deleteUserTarget($userId: String!, $targetId: String!){ + usersDeleteTarget(userId: $userId, targetId: $targetId) { + status + } + }'; case self::$GET_LOCALE: return 'query getLocale { localeGet { @@ -1688,6 +1789,439 @@ trait Base status } }'; + 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) { + _id + name + provider + type + enabled + } + }'; + 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) { + _id + name + provider + type + enabled + } + }'; + case self::$CREATE_TWILIO_PROVIDER: + return 'mutation createTwilioProvider($providerId: String!, $name: String!, $from: String!, $accountSid: String!, $authToken: String!) { + messagingCreateTwilioProvider(providerId: $providerId, name: $name, from: $from, accountSid: $accountSid, authToken: $authToken) { + _id + name + provider + type + enabled + } + }'; + case self::$CREATE_TELESIGN_PROVIDER: + return 'mutation createTelesignProvider($providerId: String!, $name: String!, $from: String!, $username: String!, $password: String!) { + messagingCreateTelesignProvider(providerId: $providerId, name: $name, from: $from, username: $username, password: $password) { + _id + name + provider + type + enabled + } + }'; + case self::$CREATE_TEXTMAGIC_PROVIDER: + return 'mutation createTextmagicProvider($providerId: String!, $name: String!, $from: String!, $username: String!, $apiKey: String!) { + messagingCreateTextmagicProvider(providerId: $providerId, name: $name, from: $from, username: $username, apiKey: $apiKey) { + _id + name + provider + type + enabled + } + }'; + case self::$CREATE_MSG91_PROVIDER: + return 'mutation createMsg91Provider($providerId: String!, $name: String!, $from: String!, $senderId: String!, $authKey: String!, $enabled: Boolean) { + messagingCreateMsg91Provider(providerId: $providerId, name: $name, from: $from, senderId: $senderId, authKey: $authKey, enabled: $enabled) { + _id + name + provider + type + enabled + } + }'; + case self::$CREATE_VONAGE_PROVIDER: + return 'mutation createVonageProvider($providerId: String!, $name: String!, $from: String!, $apiKey: String!, $apiSecret: String!) { + messagingCreateVonageProvider(providerId: $providerId, name: $name, from: $from, apiKey: $apiKey, apiSecret: $apiSecret) { + _id + name + provider + type + enabled + } + }'; + case self::$CREATE_FCM_PROVIDER: + return 'mutation createFcmProvider($providerId: String!, $name: String!, $serverKey: String!) { + messagingCreateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) { + _id + name + provider + type + enabled + } + }'; + case self::$CREATE_APNS_PROVIDER: + return 'mutation createApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) { + messagingCreateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) { + _id + name + provider + type + enabled + } + }'; + case self::$LIST_PROVIDERS: + return 'query listProviders { + messagingListProviders { + total + providers { + _id + name + provider + type + + enabled + } + } + }'; + case self::$GET_PROVIDER: + return 'query getProvider($providerId: String!) { + messagingGetProvider(providerId: $providerId) { + _id + name + provider + type + enabled + } + }'; + 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) { + _id + name + provider + type + enabled + } + }'; + case self::$UPDATE_SENDGRID_PROVIDER: + return 'mutation messagingUpdateSendgridProvider($providerId: String!, $name: String!, $apiKey: String!) { + messagingUpdateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey) { + _id + name + provider + type + enabled + } + }'; + case self::$UPDATE_TWILIO_PROVIDER: + return 'mutation updateTwilioProvider($providerId: String!, $name: String!, $accountSid: String!, $authToken: String!) { + messagingUpdateTwilioProvider(providerId: $providerId, name: $name, accountSid: $accountSid, authToken: $authToken) { + _id + name + provider + type + enabled + } + }'; + case self::$UPDATE_TELESIGN_PROVIDER: + return 'mutation updateTelesignProvider($providerId: String!, $name: String!, $username: String!, $password: String!) { + messagingUpdateTelesignProvider(providerId: $providerId, name: $name, username: $username, password: $password) { + _id + name + provider + type + enabled + } + }'; + case self::$UPDATE_TEXTMAGIC_PROVIDER: + return 'mutation updateTextmagicProvider($providerId: String!, $name: String!, $username: String!, $apiKey: String!) { + messagingUpdateTextmagicProvider(providerId: $providerId, name: $name, username: $username, apiKey: $apiKey) { + _id + name + provider + type + enabled + } + }'; + case self::$UPDATE_MSG91_PROVIDER: + return 'mutation updateMsg91Provider($providerId: String!, $name: String!, $senderId: String!, $authKey: String!) { + messagingUpdateMsg91Provider(providerId: $providerId, name: $name, senderId: $senderId, authKey: $authKey) { + _id + name + provider + type + enabled + } + }'; + case self::$UPDATE_VONAGE_PROVIDER: + return 'mutation updateVonageProvider($providerId: String!, $name: String!, $apiKey: String!, $apiSecret: String!) { + messagingUpdateVonageProvider(providerId: $providerId, name: $name, apiKey: $apiKey, apiSecret: $apiSecret) { + _id + name + provider + type + enabled + } + }'; + case self::$UPDATE_FCM_PROVIDER: + return 'mutation updateFcmProvider($providerId: String!, $name: String!, $serverKey: String!) { + messagingUpdateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) { + _id + name + provider + type + enabled + } + }'; + case self::$UPDATE_APNS_PROVIDER: + 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 + type + enabled + } + }'; + case self::$DELETE_PROVIDER: + return 'mutation deleteProvider($providerId: String!) { + messagingDeleteProvider(providerId: $providerId) { + status + } + }'; + case self::$CREATE_TOPIC: + return 'mutation createTopic($topicId: String!, $name: String!, $description: String!) { + messagingCreateTopic(topicId: $topicId, name: $name, description: $description) { + _id + name + description + } + }'; + case self::$LIST_TOPICS: + return 'query listTopics { + messagingListTopics { + total + topics { + _id + name + description + } + } + }'; + case self::$GET_TOPIC: + return 'query getTopic($topicId: String!) { + messagingGetTopic(topicId: $topicId) { + _id + name + description + } + }'; + case self::$UPDATE_TOPIC: + return 'mutation updateTopic($topicId: String!, $name: String!, $description: String!) { + messagingUpdateTopic(topicId: $topicId, name: $name, description: $description) { + _id + name + description + } + }'; + case self::$DELETE_TOPIC: + return 'mutation deleteTopic($topicId: String!) { + messagingDeleteTopic(topicId: $topicId) { + status + } + }'; + case self::$CREATE_SUBSCRIBER: + return 'mutation createSubscriber($subscriberId: String!, $targetId: String!, $topicId: String!) { + messagingCreateSubscriber(subscriberId: $subscriberId, targetId: $targetId, topicId: $topicId) { + _id + targetId + topicId + userName + target { + _id + userId + name + providerType + identifier + } + } + }'; + case self::$LIST_SUBSCRIBERS: + return 'query listSubscribers($topicId: String!) { + messagingListSubscribers(topicId: $topicId) { + total + subscribers { + _id + targetId + topicId + userName + target { + _id + userId + name + providerType + identifier + } + } + } + }'; + case self::$GET_SUBSCRIBER: + return 'query getSubscriber($topicId: String!, $subscriberId: String!) { + messagingGetSubscriber(topicId: $topicId, subscriberId: $subscriberId) { + _id + targetId + topicId + userName + target { + _id + userId + name + providerType + identifier + } + } + }'; + case self::$DELETE_SUBSCRIBER: + return 'mutation deleteSubscriber($topicId: String!, $subscriberId: String!) { + messagingDeleteSubscriber(topicId: $topicId, subscriberId: $subscriberId) { + status + } + }'; + 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) { + _id + topics + users + targets + scheduledAt + deliveredAt + deliveryErrors + deliveredTotal + status + description + } + }'; + case self::$CREATE_SMS: + return 'mutation createSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String!, $status: String, $description: String, $scheduledAt: String) { + messagingCreateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, scheduledAt: $scheduledAt) { + _id + topics + users + targets + scheduledAt + deliveredAt + deliveryErrors + deliveredTotal + status + description + } + }'; + case self::$CREATE_PUSH_NOTIFICATION: + return 'mutation createPushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String!, $body: String!, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $scheduledAt: String) { + messagingCreatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, scheduledAt: $scheduledAt) { + _id + topics + users + targets + scheduledAt + deliveredAt + deliveryErrors + deliveredTotal + status + description + } + }'; + case self::$LIST_MESSAGES: + return 'query listMessages { + messagingListMessages { + total + messages { + _id + providerType + topics + users + targets + scheduledAt + deliveredAt + deliveryErrors + deliveredTotal + status + description + } + } + }'; + case self::$GET_MESSAGE: + return 'query getMessage($messageId: String!) { + messagingGetMessage(messageId: $messageId) { + _id + providerType + topics + users + targets + scheduledAt + deliveredAt + deliveryErrors + deliveredTotal + status + description + } + }'; + 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) { + _id + topics + users + targets + scheduledAt + deliveredAt + deliveryErrors + deliveredTotal + status + description + } + }'; + case self::$UPDATE_SMS: + return 'mutation updateSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String, $status: String, $description: String, $scheduledAt: String) { + messagingUpdateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, scheduledAt: $scheduledAt) { + _id + topics + users + targets + scheduledAt + deliveredAt + deliveryErrors + deliveredTotal + status + description + } + }'; + case self::$UPDATE_PUSH_NOTIFICATION: + return 'mutation updatePushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String, $body: String, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $scheduledAt: String) { + messagingUpdatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, scheduledAt: $scheduledAt) { + _id + topics + users + targets + scheduledAt + deliveredAt + deliveryErrors + deliveredTotal + status + description + } + }'; case self::$COMPLEX_QUERY: return 'mutation complex($databaseId: String!, $databaseName: String!, $collectionId: String!, $collectionName: String!, $documentSecurity: Boolean!, $collectionPermissions: [String!]!) { databasesCreate(databaseId: $databaseId, name: $databaseName) { @@ -1940,7 +2474,7 @@ trait Base protected string $stdout = ''; protected string $stderr = ''; - protected function packageCode($folder) + protected function packageCode($folder): void { Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr); } diff --git a/tests/e2e/Services/GraphQL/MessagingTest.php b/tests/e2e/Services/GraphQL/MessagingTest.php new file mode 100644 index 0000000000..d1a084cfc1 --- /dev/null +++ b/tests/e2e/Services/GraphQL/MessagingTest.php @@ -0,0 +1,1153 @@ + [ + 'providerId' => ID::unique(), + 'name' => 'Sengrid1', + 'apiKey' => 'my-apikey', + 'from' => 'sender-email@my-domain.com', + ], + 'Mailgun' => [ + 'providerId' => ID::unique(), + 'name' => 'Mailgun1', + 'apiKey' => 'my-apikey', + 'domain' => 'my-domain', + 'from' => 'sender-email@my-domain.com', + 'isEuRegion' => false, + ], + 'Twilio' => [ + 'providerId' => ID::unique(), + 'name' => 'Twilio1', + 'accountSid' => 'my-accountSid', + 'authToken' => 'my-authToken', + 'from' => '+123456789', + ], + 'Telesign' => [ + 'providerId' => ID::unique(), + 'name' => 'Telesign1', + 'username' => 'my-username', + 'password' => 'my-password', + 'from' => '+123456789', + ], + 'Textmagic' => [ + 'providerId' => ID::unique(), + 'name' => 'Textmagic1', + 'username' => 'my-username', + 'apiKey' => 'my-apikey', + 'from' => '+123456789', + ], + 'Msg91' => [ + 'providerId' => ID::unique(), + 'name' => 'Ms91-1', + 'senderId' => 'my-senderid', + 'authKey' => 'my-authkey', + 'from' => '+123456789' + ], + 'Vonage' => [ + 'providerId' => ID::unique(), + 'name' => 'Vonage1', + 'apiKey' => 'my-apikey', + 'apiSecret' => 'my-apisecret', + 'from' => '+123456789', + ], + 'Fcm' => [ + 'providerId' => ID::unique(), + 'name' => 'FCM1', + 'serverKey' => 'my-serverkey', + ], + 'Apns' => [ + 'providerId' => ID::unique(), + 'name' => 'APNS1', + 'authKey' => 'my-authkey', + 'authKeyId' => 'my-authkeyid', + 'teamId' => 'my-teamid', + 'bundleId' => 'my-bundleid', + 'endpoint' => 'my-endpoint', + ], + ]; + + $providers = []; + + foreach (\array_keys($providersParams) as $key) { + $query = $this->getQuery('create_' . \strtolower($key) . '_provider'); + $graphQLPayload = [ + 'query' => $query, + 'variables' => $providersParams[$key], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + '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']); + } + + return $providers; + } + + /** + * @depends testCreateProviders + */ + public function testUpdateProviders(array $providers): array + { + $providersParams = [ + 'Sendgrid' => [ + 'providerId' => $providers[0]['_id'], + 'name' => 'Sengrid2', + 'apiKey' => 'my-apikey', + ], + 'Mailgun' => [ + 'providerId' => $providers[1]['_id'], + 'name' => 'Mailgun2', + 'apiKey' => 'my-apikey', + 'domain' => 'my-domain', + ], + 'Twilio' => [ + 'providerId' => $providers[2]['_id'], + 'name' => 'Twilio2', + 'accountSid' => 'my-accountSid', + 'authToken' => 'my-authToken', + ], + 'Telesign' => [ + 'providerId' => $providers[3]['_id'], + 'name' => 'Telesign2', + 'username' => 'my-username', + 'password' => 'my-password', + ], + 'Textmagic' => [ + 'providerId' => $providers[4]['_id'], + 'name' => 'Textmagic2', + 'username' => 'my-username', + 'apiKey' => 'my-apikey', + ], + 'Msg91' => [ + 'providerId' => $providers[5]['_id'], + 'name' => 'Ms91-2', + 'senderId' => 'my-senderid', + 'authKey' => 'my-authkey', + ], + 'Vonage' => [ + 'providerId' => $providers[6]['_id'], + 'name' => 'Vonage2', + 'apiKey' => 'my-apikey', + 'apiSecret' => 'my-apisecret', + ], + 'Fcm' => [ + 'providerId' => $providers[7]['_id'], + 'name' => 'FCM2', + 'serverKey' => 'my-serverkey', + ], + 'Apns' => [ + 'providerId' => $providers[8]['_id'], + 'name' => 'APNS2', + 'authKey' => 'my-authkey', + 'authKeyId' => 'my-authkeyid', + 'teamId' => 'my-teamid', + 'bundleId' => 'my-bundleid', + 'endpoint' => 'my-endpoint', + ], + ]; + foreach (\array_keys($providersParams) as $index => $key) { + $query = $this->getQuery('update_' . \strtolower($key) . '_provider'); + $graphQLPayload = [ + 'query' => $query, + 'variables' => $providersParams[$key], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $graphQLPayload); + $providers[$index] = $response['body']['data']['messagingUpdate' . $key . 'Provider']; + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($providersParams[$key]['name'], $response['body']['data']['messagingUpdate' . $key . 'Provider']['name']); + } + + $response = $this->client->call(Client::METHOD_POST, '/graphql', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'query' => $this->getQuery('update_mailgun_provider'), + 'variables' => [ + 'providerId' => $providers[1]['_id'], + 'name' => 'Mailgun2', + 'apiKey' => 'my-apikey', + 'domain' => 'my-domain', + 'isEuRegion' => true, + 'enabled' => false, + ] + ]); + $providers[1] = $response['body']['data']['messagingUpdateMailgunProvider']; + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Mailgun2', $response['body']['data']['messagingUpdateMailgunProvider']['name']); + $this->assertEquals(false, $response['body']['data']['messagingUpdateMailgunProvider']['enabled']); + return $providers; + } + + /** + * @depends testUpdateProviders + */ + public function testListProviders(array $providers) + { + $query = $this->getQuery(self::$LIST_PROVIDERS); + $graphQLPayload = [ + 'query' => $query, + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $graphQLPayload); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(\count($providers), \count($response['body']['data']['messagingListProviders']['providers'])); + } + + /** + * @depends testUpdateProviders + */ + public function testGetProvider(array $providers) + { + $query = $this->getQuery(self::$GET_PROVIDER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'providerId' => $providers[0]['_id'], + ] + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $graphQLPayload); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($providers[0]['name'], $response['body']['data']['messagingGetProvider']['name']); + } + + /** + * @depends testUpdateProviders + */ + public function testDeleteProvider(array $providers) + { + foreach ($providers as $provider) { + $query = $this->getQuery(self::$DELETE_PROVIDER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'providerId' => $provider['_id'], + ] + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $graphQLPayload); + $this->assertEquals(204, $response['headers']['status-code']); + } + } + + public function testCreateTopic() + { + $query = $this->getQuery(self::$CREATE_TOPIC); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'topicId' => ID::unique(), + 'name' => 'topic1', + 'description' => 'Active users', + ], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('topic1', $response['body']['data']['messagingCreateTopic']['name']); + $this->assertEquals('Active users', $response['body']['data']['messagingCreateTopic']['description']); + + return $response['body']['data']['messagingCreateTopic']; + } + + /** + * @depends testCreateTopic + */ + public function testUpdateTopic(array $topic) + { + $topicId = $topic['_id']; + $query = $this->getQuery(self::$UPDATE_TOPIC); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'topicId' => $topicId, + 'name' => 'topic2', + 'description' => 'Inactive users', + ], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('topic2', $response['body']['data']['messagingUpdateTopic']['name']); + $this->assertEquals('Inactive users', $response['body']['data']['messagingUpdateTopic']['description']); + + return $topicId; + } + + /** + * @depends testCreateTopic + */ + public function testListTopics() + { + $query = $this->getQuery(self::$LIST_TOPICS); + $graphQLPayload = [ + 'query' => $query, + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, \count($response['body']['data']['messagingListTopics']['topics'])); + } + + /** + * @depends testUpdateTopic + */ + public function testGetTopic(string $topicId) + { + $query = $this->getQuery(self::$GET_TOPIC); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'topicId' => $topicId, + ], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('topic2', $response['body']['data']['messagingGetTopic']['name']); + $this->assertEquals('Inactive users', $response['body']['data']['messagingGetTopic']['description']); + } + + /** + * @depends testCreateTopic + */ + public function testCreateSubscriber(array $topic) + { + $topicId = $topic['_id']; + + $userId = $this->getUser()['$id']; + + $providerParam = [ + 'sendgrid' => [ + 'providerId' => ID::unique(), + 'name' => 'Sengrid1', + 'apiKey' => 'my-apikey', + 'from' => 'sender-email@my-domain.com', + ] + ]; + $query = $this->getQuery(self::$CREATE_SENDGRID_PROVIDER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => $providerParam['sendgrid'], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $providerId = $response['body']['data']['messagingCreateSendgridProvider']['_id']; + + $query = $this->getQuery(self::$CREATE_USER_TARGET); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'targetId' => ID::unique(), + 'providerType' => 'email', + 'userId' => $userId, + 'providerId' => $providerId, + 'identifier' => 'random-email@mail.org', + ], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($userId, $response['body']['data']['usersCreateTarget']['userId']); + $this->assertEquals('random-email@mail.org', $response['body']['data']['usersCreateTarget']['identifier']); + + $targetId = $response['body']['data']['usersCreateTarget']['_id']; + + $query = $this->getQuery(self::$CREATE_SUBSCRIBER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'subscriberId' => ID::unique(), + 'topicId' => $topicId, + 'targetId' => $targetId, + ], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['topicId'], $topicId); + $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['targetId'], $targetId); + $this->assertEquals($response['body']['data']['messagingCreateSubscriber']['target']['userId'], $userId); + + return $response['body']['data']['messagingCreateSubscriber']; + } + + /** + * @depends testCreateSubscriber + */ + public function testListSubscribers(array $subscriber) + { + $query = $this->getQuery(self::$LIST_SUBSCRIBERS); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'topicId' => $subscriber['topicId'], + ], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($subscriber['topicId'], $response['body']['data']['messagingListSubscribers']['subscribers'][0]['topicId']); + $this->assertEquals($subscriber['targetId'], $response['body']['data']['messagingListSubscribers']['subscribers'][0]['targetId']); + $this->assertEquals($subscriber['target']['userId'], $response['body']['data']['messagingListSubscribers']['subscribers'][0]['target']['userId']); + $this->assertEquals(1, \count($response['body']['data']['messagingListSubscribers']['subscribers'])); + } + + /** + * @depends testCreateSubscriber + */ + public function testGetSubscriber(array $subscriber) + { + $topicId = $subscriber['topicId']; + $subscriberId = $subscriber['_id']; + + $query = $this->getQuery(self::$GET_SUBSCRIBER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'topicId' => $topicId, + 'subscriberId' => $subscriberId, + ], + ]; + + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($subscriberId, $response['body']['data']['messagingGetSubscriber']['_id']); + $this->assertEquals($topicId, $response['body']['data']['messagingGetSubscriber']['topicId']); + $this->assertEquals($subscriber['targetId'], $response['body']['data']['messagingGetSubscriber']['targetId']); + $this->assertEquals($subscriber['target']['userId'], $response['body']['data']['messagingGetSubscriber']['target']['userId']); + } + + /** + * @depends testCreateSubscriber + */ + public function testDeleteSubscriber(array $subscriber) + { + $topicId = $subscriber['topicId']; + $subscriberId = $subscriber['_id']; + + $query = $this->getQuery(self::$DELETE_SUBSCRIBER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'topicId' => $topicId, + 'subscriberId' => $subscriberId, + ], + ]; + + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $response['headers']['status-code']); + } + + /** + * @depends testUpdateTopic + */ + public function testDeleteTopic(string $topicId) + { + $query = $this->getQuery(self::$DELETE_TOPIC); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'topicId' => $topicId, + ], + ]; + $response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(204, $response['headers']['status-code']); + } + + public function testSendEmail() + { + if (empty(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'))) { + $this->markTestSkipped('Email DSN not provided'); + } + + $emailDSN = new DSN(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN')); + $to = $emailDSN->getParam('to'); + $from = $emailDSN->getParam('from'); + $isEuRegion = $emailDSN->getParam('isEuRegion'); + $apiKey = $emailDSN->getPassword(); + $domain = $emailDSN->getUser(); + + if (empty($to) || empty($from) || empty($apiKey) || empty($domain) || empty($isEuRegion)) { + $this->markTestSkipped('Email provider not configured'); + } + + $query = $this->getQuery(self::$CREATE_MAILGUN_PROVIDER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'providerId' => ID::unique(), + 'name' => 'Mailgun1', + 'apiKey' => $apiKey, + 'domain' => $domain, + 'from' => $from, + 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN), + ], + ]; + $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $provider['headers']['status-code']); + + $providerId = $provider['body']['data']['messagingCreateMailgunProvider']['_id']; + + $query = $this->getQuery(self::$CREATE_TOPIC); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'topicId' => ID::unique(), + 'name' => 'topic1', + 'description' => 'Active users', + ], + ]; + $topic = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $topic['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_USER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'userId' => ID::unique(), + 'email' => 'random1-mail@mail.org', + 'password' => 'password', + 'name' => 'Messaging User', + ] + ]; + $user = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $user['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_USER_TARGET); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'targetId' => ID::unique(), + 'providerType' => 'email', + 'userId' => $user['body']['data']['usersCreate']['_id'], + 'providerId' => $providerId, + 'identifier' => $to, + ], + ]; + $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $target['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_SUBSCRIBER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'subscriberId' => ID::unique(), + 'topicId' => $topic['body']['data']['messagingCreateTopic']['_id'], + 'targetId' => $target['body']['data']['usersCreateTarget']['_id'], + ], + ]; + $subscriber = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $subscriber['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_EMAIL); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => ID::unique(), + 'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']], + 'subject' => 'Khali beats Undertaker', + 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + ], + ]; + $email = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $email['headers']['status-code']); + + \sleep(5); + + $query = $this->getQuery(self::$GET_MESSAGE); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => $email['body']['data']['messagingCreateEmail']['_id'], + ], + ]; + $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors'])); + + return $message['body']['data']['messagingGetMessage']; + } + + /** + * @depends testSendEmail + */ + public function testUpdateEmail(array $email) + { + $query = $this->getQuery(self::$CREATE_EMAIL); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => ID::unique(), + 'status' => 'draft', + 'topics' => [$email['topics'][0]], + 'subject' => 'Khali beats Undertaker', + 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + ], + ]; + $email = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $email['headers']['status-code']); + + $query = $this->getQuery(self::$UPDATE_EMAIL); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => $email['body']['data']['messagingCreateEmail']['_id'], + 'status' => 'processing', + ], + ]; + $email = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $email['headers']['status-code']); + + \sleep(5); + + $query = $this->getQuery(self::$GET_MESSAGE); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => $email['body']['data']['messagingUpdateEmail']['_id'], + ], + ]; + $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors'])); + } + + public function testSendSMS() + { + if (empty(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'))) { + $this->markTestSkipped('SMS DSN not provided'); + } + + $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN')); + $to = $smsDSN->getParam('to'); + $from = $smsDSN->getParam('from'); + $authKey = $smsDSN->getPassword(); + $senderId = $smsDSN->getUser(); + + if (empty($to) || empty($from) || empty($senderId) || empty($authKey)) { + $this->markTestSkipped('SMS provider not configured'); + } + + $query = $this->getQuery(self::$CREATE_MSG91_PROVIDER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'providerId' => ID::unique(), + 'name' => 'Msg91-1', + 'senderId' => $senderId, + 'authKey' => $authKey, + 'from' => $from, + ], + ]; + $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $provider['headers']['status-code']); + + $providerId = $provider['body']['data']['messagingCreateMsg91Provider']['_id']; + + $query = $this->getQuery(self::$CREATE_TOPIC); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'topicId' => ID::unique(), + 'name' => 'topic1', + 'description' => 'Active users', + ], + ]; + $topic = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $topic['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_USER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'userId' => ID::unique(), + 'email' => 'random3-email@mail.org', + 'password' => 'password', + 'name' => 'Messaging User', + ] + ]; + $user = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $user['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_USER_TARGET); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'targetId' => ID::unique(), + 'providerType' => 'sms', + 'userId' => $user['body']['data']['usersCreate']['_id'], + 'providerId' => $providerId, + 'identifier' => $to, + ], + ]; + $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $target['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_SUBSCRIBER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'subscriberId' => ID::unique(), + 'topicId' => $topic['body']['data']['messagingCreateTopic']['_id'], + 'targetId' => $target['body']['data']['usersCreateTarget']['_id'], + ], + ]; + $subscriber = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $subscriber['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_SMS); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => ID::unique(), + 'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']], + 'content' => '454665', + ], + ]; + $sms = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $sms['headers']['status-code']); + + \sleep(5); + + $query = $this->getQuery(self::$GET_MESSAGE); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => $sms['body']['data']['messagingCreateSMS']['_id'], + ], + ]; + $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors'])); + return $message['body']['data']['messagingGetMessage']; + } + + /** + * @depends testSendSMS + */ + public function testUpdateSMS(array $sms) + { + $query = $this->getQuery(self::$CREATE_SMS); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => ID::unique(), + 'status' => 'draft', + 'topics' => [$sms['topics'][0]], + 'content' => '345463', + ], + ]; + $sms = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $sms['headers']['status-code']); + + $query = $this->getQuery(self::$UPDATE_SMS); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => $sms['body']['data']['messagingCreateSMS']['_id'], + 'status' => 'processing', + ], + ]; + $sms = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $sms['headers']['status-code']); + + \sleep(5); + + $query = $this->getQuery(self::$GET_MESSAGE); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => $sms['body']['data']['messagingUpdateSMS']['_id'], + ], + ]; + $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors'])); + } + + public function testSendPushNotification() + { + if (empty(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'))) { + $this->markTestSkipped('Push DSN empty'); + } + + $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); + $to = $pushDSN->getParam('to'); + $serverKey = $pushDSN->getPassword(); + + if (empty($to) || empty($serverKey)) { + $this->markTestSkipped('Push provider not configured'); + } + + $query = $this->getQuery(self::$CREATE_FCM_PROVIDER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'providerId' => ID::unique(), + 'name' => 'FCM1', + 'serverKey' => $serverKey, + ], + ]; + $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $provider['headers']['status-code']); + + $providerId = $provider['body']['data']['messagingCreateFcmProvider']['_id']; + + $query = $this->getQuery(self::$CREATE_TOPIC); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'topicId' => ID::unique(), + 'name' => 'topic1', + 'description' => 'Active users', + ], + ]; + $topic = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $topic['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_USER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'userId' => ID::unique(), + 'email' => 'random5-mail@mail.org', + 'password' => 'password', + 'name' => 'Messaging User', + ] + ]; + $user = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $user['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_USER_TARGET); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'targetId' => ID::unique(), + 'providerType' => 'push', + 'userId' => $user['body']['data']['usersCreate']['_id'], + 'providerId' => $providerId, + 'identifier' => $to, + ], + ]; + $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $target['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_SUBSCRIBER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'subscriberId' => ID::unique(), + 'topicId' => $topic['body']['data']['messagingCreateTopic']['_id'], + 'targetId' => $target['body']['data']['usersCreateTarget']['_id'], + ], + ]; + $subscriber = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $subscriber['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_PUSH_NOTIFICATION); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => ID::unique(), + 'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']], + 'title' => 'Push Notification Title', + 'body' => 'Push Notifiaction Body', + ], + ]; + $push = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $push['headers']['status-code']); + + \sleep(5); + + $query = $this->getQuery(self::$GET_MESSAGE); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => $push['body']['data']['messagingCreatePushNotification']['_id'], + ], + ]; + $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors'])); + + return $message['body']['data']['messagingGetMessage']; + } + + /** + * @depends testSendPushNotification + */ + public function testUpdatePushNotification(array $push) + { + $query = $this->getQuery(self::$CREATE_PUSH_NOTIFICATION); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => ID::unique(), + 'status' => 'draft', + 'topics' => [$push['topics'][0]], + 'title' => 'Push Notification Title', + 'body' => 'Push Notifiaction Body', + ], + ]; + $push = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $push['headers']['status-code']); + + $query = $this->getQuery(self::$UPDATE_PUSH_NOTIFICATION); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => $push['body']['data']['messagingCreatePushNotification']['_id'], + 'status' => 'processing', + ], + ]; + $push = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $push['headers']['status-code']); + + \sleep(5); + + $query = $this->getQuery(self::$GET_MESSAGE); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'messageId' => $push['body']['data']['messagingUpdatePushNotification']['_id'], + ], + ]; + $message = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), $graphQLPayload); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['data']['messagingGetMessage']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['data']['messagingGetMessage']['deliveryErrors'])); + } +} diff --git a/tests/e2e/Services/GraphQL/UsersTest.php b/tests/e2e/Services/GraphQL/UsersTest.php index 9bd503df0f..1dac9123a9 100644 --- a/tests/e2e/Services/GraphQL/UsersTest.php +++ b/tests/e2e/Services/GraphQL/UsersTest.php @@ -45,6 +45,56 @@ class UsersTest extends Scope return $user; } + /** + * @depends testCreateUser + */ + public function testCreateUserTarget(array $user) + { + $projectId = $this->getProject()['$id']; + + $query = $this->getQuery(self::$CREATE_MAILGUN_PROVIDER); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'providerId' => ID::unique(), + 'name' => 'Mailgun1', + 'apiKey' => 'api-key', + 'domain' => 'domain', + 'from' => 'from@domain.com', + 'isEuRegion' => false, + ], + ]; + $provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + $providerId = $provider['body']['data']['messagingCreateMailgunProvider']['_id']; + + $this->assertEquals(200, $provider['headers']['status-code']); + + $query = $this->getQuery(self::$CREATE_USER_TARGET); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'targetId' => ID::unique(), + 'userId' => $user['_id'], + 'providerType' => 'email', + 'providerId' => $providerId, + 'identifier' => 'random-email@mail.org', + ] + ]; + + $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $target['headers']['status-code']); + $this->assertEquals('random-email@mail.org', $target['body']['data']['usersCreateTarget']['identifier']); + + return $target['body']['data']['usersCreateTarget']; + } + public function testGetUsers() { $projectId = $this->getProject()['$id']; @@ -176,6 +226,54 @@ class UsersTest extends Scope $this->assertIsArray($user['body']['data']['usersListLogs']); } + /** + * @depends testCreateUserTarget + */ + public function testListUserTargets(array $target) + { + $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::$LIST_USER_TARGETS); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'userId' => $target['userId'], + ] + ]; + + $targets = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $targets['headers']['status-code']); + $this->assertIsArray($targets['body']['data']['usersListTargets']); + $this->assertCount(2, $targets['body']['data']['usersListTargets']['targets']); + } + + /** + * @depends testCreateUserTarget + */ + public function testGetUserTarget(array $target) + { + $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::$GET_USER_TARGET); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'userId' => $target['userId'], + 'targetId' => $target['_id'], + ] + ]; + + $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $target['headers']['status-code']); + $this->assertEquals('random-email@mail.org', $target['body']['data']['usersGetTarget']['identifier']); + } + public function testUpdateUserStatus() { $projectId = $this->getProject()['$id']; @@ -360,6 +458,31 @@ class UsersTest extends Scope $this->assertEquals('{"key":"value"}', $user['body']['data']['usersUpdatePrefs']['data']); } + /** + * @depends testCreateUserTarget + */ + public function testUpdateUserTarget(array $target) + { + $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::$UPDATE_USER_TARGET); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'userId' => $target['userId'], + 'targetId' => $target['_id'], + 'identifier' => 'random-email1@mail.org', + ], + ]; + + $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $target['headers']['status-code']); + $this->assertEquals('random-email1@mail.org', $target['body']['data']['usersUpdateTarget']['identifier']); + } + public function testDeleteUserSessions() { $projectId = $this->getProject()['$id']; @@ -407,6 +530,29 @@ class UsersTest extends Scope $this->getUser(); } + /** + * @depends testCreateUserTarget + */ + public function testDeleteUserTarget(array $target) + { + $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::$DELETE_USER_TARGET); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'userId' => $target['userId'], + 'targetId' => $target['_id'], + ] + ]; + + $target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(204, $target['headers']['status-code']); + } + public function testDeleteUser() { $projectId = $this->getProject()['$id']; diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php new file mode 100644 index 0000000000..690e503e77 --- /dev/null +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -0,0 +1,1040 @@ + [ + 'providerId' => ID::unique(), + 'name' => 'Sengrid1', + 'apiKey' => 'my-apikey', + 'from' => 'sender-email@my-domain.com', + ], + 'mailgun' => [ + 'providerId' => ID::unique(), + 'name' => 'Mailgun1', + 'apiKey' => 'my-apikey', + 'domain' => 'my-domain', + 'from' => 'sender-email@my-domain.com', + 'isEuRegion' => false, + ], + 'twilio' => [ + 'providerId' => ID::unique(), + 'name' => 'Twilio1', + 'accountSid' => 'my-accountSid', + 'authToken' => 'my-authToken', + 'from' => '+123456789', + ], + 'telesign' => [ + 'providerId' => ID::unique(), + 'name' => 'Telesign1', + 'username' => 'my-username', + 'password' => 'my-password', + 'from' => '+123456789', + ], + 'textmagic' => [ + 'providerId' => ID::unique(), + 'name' => 'Textmagic1', + 'username' => 'my-username', + 'apiKey' => 'my-apikey', + 'from' => '+123456789', + ], + 'msg91' => [ + 'providerId' => ID::unique(), + 'name' => 'Ms91-1', + 'senderId' => 'my-senderid', + 'authKey' => 'my-authkey', + 'from' => '+123456789' + ], + 'vonage' => [ + 'providerId' => ID::unique(), + 'name' => 'Vonage1', + 'apiKey' => 'my-apikey', + 'apiSecret' => 'my-apisecret', + 'from' => '+123456789', + ], + 'fcm' => [ + 'providerId' => ID::unique(), + 'name' => 'FCM1', + 'serverKey' => 'my-serverkey', + ], + 'apns' => [ + 'providerId' => ID::unique(), + 'name' => 'APNS1', + 'authKey' => 'my-authkey', + 'authKeyId' => 'my-authkeyid', + 'teamId' => 'my-teamid', + 'bundleId' => 'my-bundleid', + 'endpoint' => 'my-endpoint', + ], + ]; + $providers = []; + + foreach (\array_keys($providersParams) as $key) { + $response = $this->client->call(Client::METHOD_POST, '/messaging/providers/' . $key, \array_merge([ + 'content-type' => 'application/json', + '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']); + } + + return $providers; + } + + /** + * @depends testCreateProviders + */ + public function testUpdateProviders(array $providers): array + { + $providersParams = [ + 'sendgrid' => [ + 'name' => 'Sengrid2', + 'apiKey' => 'my-apikey', + ], + 'mailgun' => [ + 'name' => 'Mailgun2', + 'apiKey' => 'my-apikey', + 'domain' => 'my-domain', + ], + 'twilio' => [ + 'name' => 'Twilio2', + 'accountSid' => 'my-accountSid', + 'authToken' => 'my-authToken', + ], + 'telesign' => [ + 'name' => 'Telesign2', + 'username' => 'my-username', + 'password' => 'my-password', + ], + 'textmagic' => [ + 'name' => 'Textmagic2', + 'username' => 'my-username', + 'apiKey' => 'my-apikey', + ], + 'msg91' => [ + 'name' => 'Ms91-2', + 'senderId' => 'my-senderid', + 'authKey' => 'my-authkey', + ], + 'vonage' => [ + 'name' => 'Vonage2', + 'apiKey' => 'my-apikey', + 'apiSecret' => 'my-apisecret', + ], + 'fcm' => [ + 'name' => 'FCM2', + 'serverKey' => 'my-serverkey', + ], + 'apns' => [ + 'name' => 'APNS2', + 'authKey' => 'my-authkey', + 'authKeyId' => 'my-authkeyid', + 'teamId' => 'my-teamid', + 'bundleId' => 'my-bundleid', + 'endpoint' => 'my-endpoint', + ], + ]; + foreach (\array_keys($providersParams) as $index => $key) { + $response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/' . $key . '/' . $providers[$index]['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], $providersParams[$key]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($providersParams[$key]['name'], $response['body']['name']); + $providers[$index] = $response['body']; + } + + $response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/mailgun/' . $providers[1]['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + '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']); + $this->assertEquals(false, $response['body']['enabled']); + $providers[1] = $response['body']; + return $providers; + } + + /** + * @depends testUpdateProviders + */ + public function testListProviders(array $providers) + { + $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(\count($providers), \count($response['body']['providers'])); + + return $providers; + } + + /** + * @depends testUpdateProviders + */ + public function testGetProvider(array $providers) + { + $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $providers[0]['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($providers[0]['name'], $response['body']['name']); + } + + /** + * @depends testUpdateProviders + */ + public function testDeleteProvider(array $providers) + { + foreach ($providers as $provider) { + $response = $this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $provider['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals(204, $response['headers']['status-code']); + } + } + + public function testCreateTopic(): array + { + $response = $this->client->call(Client::METHOD_POST, '/messaging/topics', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'topicId' => ID::unique(), + 'name' => 'my-app', + 'description' => 'web app' + ]); + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals('my-app', $response['body']['name']); + + return $response['body']; + } + + /** + * @depends testCreateTopic + */ + public function testUpdateTopic(array $topic): string + { + $response = $this->client->call(Client::METHOD_PATCH, '/messaging/topics/' . $topic['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'name' => 'android-app', + 'description' => 'updated-description' + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('android-app', $response['body']['name']); + $this->assertEquals('updated-description', $response['body']['description']); + return $response['body']['$id']; + } + + /** + * @depends testUpdateTopic + */ + public function testListTopic(string $topicId) + { + $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'], + ], [ + 'search' => 'updated-description', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, \count($response['body']['topics'])); + + return $topicId; + } + + /** + * @depends testUpdateTopic + */ + public function testGetTopic(string $topicId) + { + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topicId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('android-app', $response['body']['name']); + $this->assertEquals('updated-description', $response['body']['description']); + $this->assertEquals(0, $response['body']['total']); + } + + /** + * @depends testCreateTopic + */ + public function testCreateSubscriber(array $topic) + { + $userId = $this->getUser()['$id']; + + $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'], + ]), [ + 'providerId' => ID::unique(), + 'name' => 'Sendgrid1', + 'apiKey' => 'my-apikey', + 'from' => 'sender-email@my-domain.com', + ]); + + $this->assertEquals(201, $provider['headers']['status-code']); + + $target = $this->client->call(Client::METHOD_POST, '/users/' . $userId . '/targets', array_merge([ + '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' => 'random-email@mail.org', + ]); + + $this->assertEquals(201, $target['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['$id'] . '/subscribers', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'subscriberId' => ID::unique(), + 'targetId' => $target['body']['$id'], + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals($target['body']['userId'], $response['body']['target']['userId']); + $this->assertEquals($target['body']['providerType'], $response['body']['target']['providerType']); + + $topic = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $topic['headers']['status-code']); + $this->assertEquals('android-app', $topic['body']['name']); + $this->assertEquals('updated-description', $topic['body']['description']); + $this->assertEquals(1, $topic['body']['total']); + + return [ + 'topicId' => $topic['body']['$id'], + 'targetId' => $target['body']['$id'], + 'userId' => $target['body']['userId'], + 'subscriberId' => $response['body']['$id'], + 'identifier' => $target['body']['identifier'], + 'providerType' => $target['body']['providerType'], + ]; + } + + /** + * @depends testCreateSubscriber + */ + public function testGetSubscriber(array $data) + { + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $data['topicId'] . '/subscribers/' . $data['subscriberId'], \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($data['topicId'], $response['body']['topicId']); + $this->assertEquals($data['targetId'], $response['body']['targetId']); + $this->assertEquals($data['userId'], $response['body']['target']['userId']); + $this->assertEquals($data['providerType'], $response['body']['target']['providerType']); + $this->assertEquals($data['identifier'], $response['body']['target']['identifier']); + } + + /** + * @depends testCreateSubscriber + */ + public function testListSubscribers(array $data) + { + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $data['topicId'] . '/subscribers', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + $this->assertEquals($data['userId'], $response['body']['subscribers'][0]['target']['userId']); + $this->assertEquals($data['providerType'], $response['body']['subscribers'][0]['target']['providerType']); + $this->assertEquals($data['identifier'], $response['body']['subscribers'][0]['target']['identifier']); + $this->assertEquals(\count($response['body']['subscribers']), $response['body']['total']); + + return $data; + } + + /** + * @depends testListSubscribers + */ + public function testGetSubscriberLogs(array $data): void + { + /** + * Test for SUCCESS + */ + $logs = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => ['limit(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertLessThanOrEqual(1, count($logs['body']['logs'])); + $this->assertIsNumeric($logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => ['offset(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => ['limit(1)', 'offset(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertLessThanOrEqual(1, count($logs['body']['logs'])); + $this->assertIsNumeric($logs['body']['total']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => ['limit(-1)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => ['offset(-1)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => ['equal("$id", "asdf")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => ['orderAsc("$id")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/subscribers/' . $data['subscriberId'] . '/logs', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'queries' => ['cursorAsc("$id")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + } + + /** + * @depends testCreateSubscriber + */ + public function testDeleteSubscriber(array $data) + { + $response = $this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $data['topicId'] . '/subscribers/' . $data['subscriberId'], \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $response['headers']['status-code']); + + $topic = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $data['topicId'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $topic['headers']['status-code']); + $this->assertEquals('android-app', $topic['body']['name']); + $this->assertEquals('updated-description', $topic['body']['description']); + $this->assertEquals(0, $topic['body']['total']); + } + + /** + * @depends testUpdateTopic + */ + public function testDeleteTopic(string $topicId) + { + $response = $this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + $this->assertEquals(204, $response['headers']['status-code']); + } + + public function testSendEmail() + { + if (empty(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'))) { + $this->markTestSkipped('Email DSN not provided'); + } + + $emailDSN = new DSN(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN')); + $to = $emailDSN->getParam('to'); + $from = $emailDSN->getParam('from'); + $isEuRegion = $emailDSN->getParam('isEuRegion'); + $apiKey = $emailDSN->getPassword(); + $domain = $emailDSN->getUser(); + + if (empty($to) || empty($from) || empty($apiKey) || empty($domain) || empty($isEuRegion)) { + $this->markTestSkipped('Email provider not configured'); + } + + // Create provider + $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/mailgun', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'providerId' => ID::unique(), + 'name' => 'Mailgun-provider', + 'apiKey' => $apiKey, + 'domain' => $domain, + 'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN), + 'from' => $from + ]); + + $this->assertEquals(201, $provider['headers']['status-code']); + + // Create Topic + $topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'topicId' => ID::unique(), + 'name' => 'topic1', + 'description' => 'Test Topic' + ]); + + $this->assertEquals(201, $topic['headers']['status-code']); + + // Create User + $user = $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' => $to, + 'password' => 'password', + 'name' => 'Messaging User', + ]); + + $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'], + ]); + + $this->assertEquals(201, $subscriber['headers']['status-code']); + + // Create Email + $email = $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(), + 'topics' => [$topic['body']['$id']], + 'subject' => 'Khali beats Undertaker', + 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + ]); + + $this->assertEquals(201, $email['headers']['status-code']); + + \sleep(5); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'], [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['deliveryErrors'])); + + return $message; + } + + /** + * @depends testSendEmail + */ + public function testUpdateEmail(array $email) + { + $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + // Test failure as the message has already been sent. + $this->assertEquals(400, $message['headers']['status-code']); + + // Create Email + $email = $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(), + 'status' => 'draft', + 'topics' => [$email['body']['topics'][0]], + 'subject' => 'Khali beats Undertaker', + 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + ]); + + $this->assertEquals(201, $email['headers']['status-code']); + + $email = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'status' => 'processing', + ]); + + $this->assertEquals(200, $email['headers']['status-code']); + + \sleep(5); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'], [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['deliveryErrors'])); + } + + public function testSendSMS() + { + if (empty(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'))) { + $this->markTestSkipped('SMS DSN not provided'); + } + + $smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN')); + $to = $smsDSN->getParam('to'); + $from = $smsDSN->getParam('from'); + $authKey = $smsDSN->getPassword(); + $senderId = $smsDSN->getUser(); + + if (empty($to) || empty($from) || empty($senderId) || empty($authKey)) { + $this->markTestSkipped('SMS provider not configured'); + } + + // Create provider + $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/msg91', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'providerId' => ID::unique(), + 'name' => 'Msg91-1', + 'senderId' => $senderId, + 'authKey' => $authKey, + 'from' => $from + ]); + + $this->assertEquals(201, $provider['headers']['status-code']); + + // Create Topic + $topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'topicId' => ID::unique(), + 'name' => 'topic1', + 'description' => 'Test Topic' + ]); + + $this->assertEquals(201, $topic['headers']['status-code']); + + // Create User + $user = $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' => 'random1-email@mail.org', + 'password' => 'password', + 'name' => 'Messaging User', + ]); + + $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' => 'sms', + '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'], + ]); + + $this->assertEquals(201, $subscriber['headers']['status-code']); + + // Create SMS + $sms = $this->client->call(Client::METHOD_POST, '/messaging/messages/sms', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'topics' => [$topic['body']['$id']], + 'content' => '064763', + ]); + + $this->assertEquals(201, $sms['headers']['status-code']); + + \sleep(5); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $sms['body']['$id'], [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['deliveryErrors'])); + + return $message; + } + + /** + * @depends testSendSMS + */ + public function testUpdateSMS(array $sms) + { + $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/sms/' . $sms['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + // Test failure as the message has already been sent. + $this->assertEquals(400, $message['headers']['status-code']); + + // Create SMS + $sms = $this->client->call(Client::METHOD_POST, '/messaging/messages/sms', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'status' => 'draft', + 'topics' => [$sms['body']['topics'][0]], + 'content' => '047487', + ]); + + $this->assertEquals(201, $sms['headers']['status-code']); + + $sms = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/sms/' . $sms['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'status' => 'processing', + ]); + + $this->assertEquals(200, $sms['headers']['status-code']); + + \sleep(5); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $sms['body']['$id'], [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['deliveryErrors'])); + } + + public function testSendPushNotification() + { + if (empty(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'))) { + $this->markTestSkipped('Push DSN empty'); + } + + $pushDSN = new DSN(App::getEnv('_APP_MESSAGE_PUSH_TEST_DSN')); + $to = $pushDSN->getParam('to'); + $serverKey = $pushDSN->getPassword(); + + if (empty($to) || empty($serverKey)) { + $this->markTestSkipped('Push provider not configured'); + } + + // Create provider + $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/fcm', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), [ + 'providerId' => ID::unique(), + 'name' => 'FCM-1', + 'serverKey' => $serverKey, + ]); + + $this->assertEquals(201, $provider['headers']['status-code']); + + // Create Topic + $topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'topicId' => ID::unique(), + 'name' => 'topic1', + 'description' => 'Test Topic' + ]); + + $this->assertEquals(201, $topic['headers']['status-code']); + + // Create User + $user = $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' => 'random3-email@mail.org', + 'password' => 'password', + 'name' => 'Messaging User', + ]); + + $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' => 'push', + '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'], + ]); + + $this->assertEquals(201, $subscriber['headers']['status-code']); + + // Create push notification + $push = $this->client->call(Client::METHOD_POST, '/messaging/messages/push', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'topics' => [$topic['body']['$id']], + 'title' => 'Test-Notification', + 'body' => 'Test-Notification-Body', + ]); + + $this->assertEquals(201, $push['headers']['status-code']); + + \sleep(5); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $push['body']['$id'], [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['deliveryErrors'])); + + return $message; + } + + /** + * @depends testSendPushNotification + */ + public function testUpdatePushNotification(array $push) + { + $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/push/' . $push['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + // Test failure as the message has already been sent. + $this->assertEquals(400, $message['headers']['status-code']); + + // Create push notification + $push = $this->client->call(Client::METHOD_POST, '/messaging/messages/push', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'status' => 'draft', + 'topics' => [$push['body']['topics'][0]], + 'title' => 'Test-Notification', + 'body' => 'Test-Notification-Body', + ]); + + $this->assertEquals(201, $push['headers']['status-code']); + + $push = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/push/' . $push['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'status' => 'processing', + ]); + + $this->assertEquals(200, $push['headers']['status-code']); + + \sleep(5); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $push['body']['$id'], [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(1, $message['body']['deliveredTotal']); + $this->assertEquals(0, \count($message['body']['deliveryErrors'])); + } +} diff --git a/tests/e2e/Services/Messaging/MessagingConsoleClientTest.php b/tests/e2e/Services/Messaging/MessagingConsoleClientTest.php new file mode 100644 index 0000000000..0baa465b48 --- /dev/null +++ b/tests/e2e/Services/Messaging/MessagingConsoleClientTest.php @@ -0,0 +1,413 @@ +client->call(Client::METHOD_GET, '/messaging/providers/' . $providers[0]['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + + $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid/', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'providerId' => ID::unique(), + 'name' => 'Sengrid1', + 'apiKey' => 'my-apikey', + 'from' => 'sender-email@my-domain.com', + ]); + + $this->assertEquals(201, $provider['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/sendgrid/' . $provider['body']['$id'], \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'sendgrid' => [ + 'name' => 'Sendgrid2', + ]]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + $this->assertCount(2, $logs['body']['logs']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertLessThanOrEqual(1, count($logs['body']['logs'])); + $this->assertIsNumeric($logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['offset(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(1)', 'offset(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertLessThanOrEqual(1, count($logs['body']['logs'])); + $this->assertIsNumeric($logs['body']['total']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(-1)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['offset(-1)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("$id", "asdf")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['orderAsc("$id")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['cursorAsc("$id")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + } + + /** + * @depends testListTopic + */ + public function testGetTopicLogs(string $topicId): void + { + /** + * Test for SUCCESS + */ + $logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topicId . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + + $topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'topicId' => ID::unique(), + 'name' => 'my-app', + 'description' => 'web app' + ]); + $this->assertEquals(201, $topic['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PATCH, '/messaging/topics/' . $topic['body']['$id'], \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'description' => 'updated-description' + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertCount(2, $logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertLessThanOrEqual(1, count($logs['body']['logs'])); + $this->assertIsNumeric($logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['offset(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(1)', 'offset(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertLessThanOrEqual(1, count($logs['body']['logs'])); + $this->assertIsNumeric($logs['body']['total']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(-1)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['offset(-1)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("$id", "asdf")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['orderAsc("$id")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['cursorAsc("$id")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + } + + /** + * @depends testSendEmail + */ + public function testGetMessageLogs(array $email): void + { + /** + * Test for SUCCESS + */ + $logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + + $email = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'messageId' => ID::unique(), + 'status' => 'draft', + 'topics' => [ID::unique()], + 'subject' => 'Khali beats Undertaker', + 'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + ]); + + $this->assertEquals(201, $email['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'subject' => 'Khali beats John Cena!', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + $this->assertCount(2, $logs['body']['logs']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertLessThanOrEqual(1, count($logs['body']['logs'])); + $this->assertIsNumeric($logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['offset(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertIsNumeric($logs['body']['total']); + + $logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(1)', 'offset(1)'], + ]); + + $this->assertEquals($logs['headers']['status-code'], 200); + $this->assertIsArray($logs['body']['logs']); + $this->assertLessThanOrEqual(1, count($logs['body']['logs'])); + $this->assertIsNumeric($logs['body']['total']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['limit(-1)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['offset(-1)'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['equal("$id", "asdf")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['orderAsc("$id")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => ['cursorAsc("$id")'] + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + } +} diff --git a/tests/e2e/Services/Messaging/MessagingCustomClientTest.php b/tests/e2e/Services/Messaging/MessagingCustomClientTest.php new file mode 100644 index 0000000000..ee7d0c2ad9 --- /dev/null +++ b/tests/e2e/Services/Messaging/MessagingCustomClientTest.php @@ -0,0 +1,14 @@ + $provider) { $asserted = false; - foreach ($response['body']['providers'] as $responseProvider) { + foreach ($response['body']['oAuthProviders'] as $responseProvider) { if ($responseProvider['key'] === $key) { $this->assertEquals('AppId-' . ucfirst($key), $responseProvider['appId']); $this->assertEquals('Secret-' . ucfirst($key), $responseProvider['secret']); @@ -867,7 +867,7 @@ class ProjectsConsoleClientTest extends Scope $i = 0; foreach ($providers as $key => $provider) { $asserted = false; - foreach ($response['body']['providers'] as $responseProvider) { + foreach ($response['body']['oAuthProviders'] as $responseProvider) { if ($responseProvider['key'] === $key) { // On first provider, test enabled=false $this->assertEquals($i !== 0, $responseProvider['enabled']); diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index 3327bb7558..7d22f23251 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -23,6 +23,7 @@ trait UsersBase 'password' => 'password', 'name' => 'Cristiano Ronaldo', ], false); + $this->assertEquals($user['headers']['status-code'], 201); // Test empty prefs is object not array $bodyString = $user['body']; @@ -1223,6 +1224,99 @@ trait UsersBase $this->assertEquals($response['headers']['status-code'], 400); } + /** + * @depends testGetUser + */ + public function testCreateUserTarget(array $data): array + { + $provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'providerId' => ID::unique(), + 'name' => 'Sengrid1', + 'apiKey' => 'my-apikey', + 'from' => 'from@domain.com', + ]); + $this->assertEquals(201, $provider['headers']['status-code']); + $response = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/targets', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'targetId' => ID::unique(), + 'providerId' => $provider['body']['$id'], + 'providerType' => 'email', + 'identifier' => 'random-email@mail.org', + ]); + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals($provider['body']['$id'], $response['body']['providerId']); + $this->assertEquals('random-email@mail.org', $response['body']['identifier']); + return $response['body']; + } + + /** + * @depends testCreateUserTarget + */ + public function testUpdateUserTarget(array $data): array + { + $response = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'identifier' => 'random-email1@mail.org', + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('random-email1@mail.org', $response['body']['identifier']); + return $response['body']; + } + + /** + * @depends testUpdateUserTarget + */ + public function testListUserTarget(array $data) + { + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(2, \count($response['body']['targets'])); + } + + /** + * @depends testUpdateUserTarget + */ + public function testGetUserTarget(array $data) + { + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($data['$id'], $response['body']['$id']); + } + + /** + * @depends testUpdateUserTarget + */ + public function testDeleteUserTarget(array $data) + { + $response = $this->client->call(Client::METHOD_DELETE, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(204, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(1, $response['body']['total']); + } + /** * @depends testGetUser */ diff --git a/tests/unit/Auth/Validator/PasswordDictionaryTest.php b/tests/unit/Auth/Validator/PasswordDictionaryTest.php index fd7f51ff16..5c8d47923c 100644 --- a/tests/unit/Auth/Validator/PasswordDictionaryTest.php +++ b/tests/unit/Auth/Validator/PasswordDictionaryTest.php @@ -24,5 +24,16 @@ class PasswordDictionaryTest extends TestCase $this->assertEquals($this->object->isValid('123456'), false); $this->assertEquals($this->object->isValid('password'), false); $this->assertEquals($this->object->isValid('myPasswordIsRight'), true); + + $pass = ''; // 256 chars + for ($i = 0; $i < 256; $i++) { + $pass .= 'p'; + } + + $this->assertEquals($this->object->isValid($pass), true); + + $pass .= 'p'; // 257 chars + + $this->assertEquals($this->object->isValid($pass), false); } } diff --git a/tests/unit/Utopia/Response/Filters/V16Test.php b/tests/unit/Utopia/Response/Filters/V16Test.php index fba3b69535..2078559b61 100644 --- a/tests/unit/Utopia/Response/Filters/V16Test.php +++ b/tests/unit/Utopia/Response/Filters/V16Test.php @@ -154,9 +154,9 @@ class V16Test extends TestCase public function projectProvider(): array { return [ - 'providers' => [ + 'oAuthProviders' => [ [ - 'providers' => [ + 'oAuthProviders' => [ [ 'key' => 'github', 'name' => 'GitHub', @@ -167,7 +167,7 @@ class V16Test extends TestCase ], ], [ - 'providers' => [ + 'oAuthProviders' => [ [ 'name' => 'Github', 'appId' => 'client_id',