1
0
Fork 0
mirror of synced 2024-09-28 15:31:43 +12:00

Merge branch '1.5.x' into fix-limit-failed-webhook-attempts

This commit is contained in:
Khushboo Verma 2023-11-20 17:40:32 +05:30
commit 2bcb6c18fb
46 changed files with 1449 additions and 1324 deletions

View file

@ -99,6 +99,7 @@ jobs:
Users,
Webhooks,
VCS,
Messaging,
]
steps:

2
.gitmodules vendored
View file

@ -1,4 +1,4 @@
[submodule "app/console"]
path = app/console
url = https://github.com/appwrite/console
branch = 3.2.6
branch = 3.2.7

View file

@ -1,3 +1,17 @@
# Version 1.4.12
## Miscellaneous
* Bump console to version 3.2.7 [#7148](https://github.com/appwrite/appwrite/pull/7148)
* Chore update database to 0.45.2 [#7138](https://github.com/appwrite/appwrite/pull/7138)
* Implement queue thresholds for the health API [#7123](https://github.com/appwrite/appwrite/pull/7123)
* Add Authorization::skip to the usage worker [#7124](https://github.com/appwrite/appwrite/pull/7124)
## Bug fixes
* fix: use queueForDeletes in git installation delete endpoint [#7140](https://github.com/appwrite/appwrite/pull/7140)
* fix: patch script, make errors silent [#7134](https://github.com/appwrite/appwrite/pull/7134)
* fix: repositories recreation script [#7133](https://github.com/appwrite/appwrite/pull/7133)
* fix: Only delete repositories linked to the particular project [#7131](https://github.com/appwrite/appwrite/pull/7131)
# Version 1.4.11
## Miscellaneous

View file

@ -100,6 +100,7 @@ RUN chmod +x /usr/local/bin/doctor && \
RUN chmod +x /usr/local/bin/hamster && \
chmod +x /usr/local/bin/volume-sync && \
chmod +x /usr/local/bin/patch-delete-schedule-updated-at-attribute && \
chmod +x /usr/local/bin/patch-recreate-repositories-documents && \
chmod +x /usr/local/bin/patch-delete-project-collections && \
chmod +x /usr/local/bin/delete-orphaned-projects && \
chmod +x /usr/local/bin/clear-card-cache && \

View file

@ -55,7 +55,7 @@ Appwrite 可以提供给开发者用户验证,外部授权,用户数据读
## 安装
Appwrite 的容器化服务器只需要一行指令就可以运行。您可以使用 docker-compose 在本地主机上运行 Appwrite也可以在任何其他容器化工具如 Kubernetes、Docker Swarm 或 Rancher上运行 Appwrite。
Appwrite 的容器化服务器只需要一行指令就可以运行。您可以使用 docker-compose 在本地主机上运行 Appwrite也可以在任何其他容器化工具[Kubernetes](https://kubernetes.io/docs/home/)[Docker Swarm](https://docs.docker.com/engine/swarm/)[Rancher](https://rancher.com/docs/))上运行 Appwrite。
启动 Appwrite 服务器的最简单方法是运行我们的 docker-compose 文件。在运行安装命令之前,请确保您的机器上安装了 [Docker](https://dockerdocs.cn/get-docker/index.html)
@ -66,7 +66,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.4.11
appwrite/appwrite:1.4.12
```
### Windows
@ -78,7 +78,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.4.11
appwrite/appwrite:1.4.12
```
#### PowerShell
@ -88,7 +88,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.4.11
appwrite/appwrite:1.4.12
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。

View file

@ -76,7 +76,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.4.11
appwrite/appwrite:1.4.12
```
### Windows
@ -88,7 +88,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.4.11
appwrite/appwrite:1.4.12
```
#### PowerShell
@ -98,7 +98,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.4.11
appwrite/appwrite:1.4.12
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.

View file

@ -1416,17 +1416,6 @@ $commonCollections = [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('internal'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => false,
'array' => false,
],
[
'$id' => ID::custom('enabled'),
'type' => Database::VAR_BOOLEAN,
@ -1467,9 +1456,9 @@ $commonCollections = [
'size' => 65535,
'signed' => true,
'required' => false,
'default' => null,
'default' => '',
'array' => false,
'filters' => [],
'filters' => ['providerSearch'],
],
],
'indexes' => [
@ -1495,16 +1484,9 @@ $commonCollections = [
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_internal'),
'$id' => ID::custom('_key_enabled_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['internal'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_internal_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['internal','type'],
'attributes' => ['enabled','type'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
@ -1640,9 +1622,9 @@ $commonCollections = [
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'default' => '',
'array' => false,
'filters' => [],
'filters' => ['messageSearch'],
],
],
'indexes' => [
@ -1712,9 +1694,9 @@ $commonCollections = [
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'default' => '',
'array' => false,
'filters' => [],
'filters' => ['topicSearch'],
],
],
'indexes' => [
@ -1740,6 +1722,28 @@ $commonCollections = [
'$id' => ID::custom('subscribers'),
'name' => 'Subscribers',
'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('targetId'),
'type' => Database::VAR_STRING,
@ -1786,6 +1790,20 @@ $commonCollections = [
],
],
'indexes' => [
[
'$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_targetId'),
'type' => Database::INDEX_KEY,
@ -1845,7 +1863,7 @@ $commonCollections = [
'filters' => [],
],
[
'$id' => ID::custom('providerId'),
'$id' => ID::custom('providerType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@ -1855,13 +1873,24 @@ $commonCollections = [
'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' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
@ -1877,6 +1906,17 @@ $commonCollections = [
'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' => [
[
@ -1909,18 +1949,11 @@ $commonCollections = [
],
[
'$id' => ID::custom('_key_identifier'),
'type' => Database::INDEX_KEY,
'type' => Database::INDEX_UNIQUE,
'attributes' => ['identifier'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_identifier_providerId'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['providerId', 'identifier'],
'lengths' => [],
'orders' => [],
]
],
],
];

View file

@ -240,6 +240,16 @@ return [
'description' => 'OAuth2 provider returned some error.',
'code' => 424,
],
Exception::USER_EMAIL_ALREADY_VERIFIED => [
'name' => Exception::USER_EMAIL_ALREADY_VERIFIED,
'description' => 'User email is already verified',
'code' => 409,
],
Exception::USER_PHONE_ALREADY_VERIFIED => [
'name' => Exception::USER_PHONE_ALREADY_VERIFIED,
'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.',

View file

@ -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,

@ -1 +1 @@
Subproject commit f7c34a1b37d53dd5f28c83b4e12a4e68fcd9b484
Subproject commit 49d039ed07628155e7f56e2c997fcef90ecde267

View file

@ -148,7 +148,20 @@ 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));
$target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerType' => 'email',
'identifier' => $email,
])));
$user->setAttribute('targets', [$target]);
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@ -656,7 +669,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' => 'email',
'identifier' => $email,
]));
} catch (Duplicate) {
$failureRedirect(Exception::USER_ALREADY_EXISTS);
}
@ -1236,11 +1260,7 @@ App::post('/v1/account/sessions/phone')
->inject('queueForMessaging')
->inject('locale')
->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale) {
$provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['sms'])
]));
if ($provider === false || $provider->isEmpty()) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@ -1326,31 +1346,18 @@ App::post('/v1/account/sessions/phone')
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
Query::equal('providerInternalId', [$provider->getInternalId()])
]);
if (!$target) {
$target = $dbForProject->createDocument('targets', new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerId' => $provider->getId(),
'providerInternalId' => $provider->getInternalId(),
'identifier' => $phone,
]));
}
$messageDoc = $dbForProject->createDocument('messages', new Document([
$messageDoc = new Document([
'$id' => $token->getId(),
'targets' => [$target->getId()],
'data' => [
'content' => $message,
],
]));
]);
$queueForMessaging
->setMessageId($messageDoc->getId())
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType('SMS')
->setProject($project)
->trigger();
@ -1670,6 +1677,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' => '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'])
@ -1995,6 +2077,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
@ -2019,6 +2102,23 @@ App::patch('/v1/account/email')
->setAttribute('passwordUpdate', DateTime::now());
}
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]);
if ($target && !$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget !== false && !$oldTarget->isEmpty()) {
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email));
}
try {
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
} catch (Duplicate) {
@ -2065,6 +2165,19 @@ App::patch('/v1/account/phone')
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]);
if ($target && !$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $user->getAttribute('phone'), 'targets');
$user
->setAttribute('phone', $phone)
->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again
@ -2078,6 +2191,10 @@ App::patch('/v1/account/phone')
->setAttribute('passwordUpdate', DateTime::now());
}
if ($oldTarget !== false && !$oldTarget->isEmpty()) {
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone));
}
try {
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
} catch (Duplicate $th) {
@ -2688,6 +2805,10 @@ App::post('/v1/account/verification')
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
}
if ($user->getAttribute('emailVerification')) {
throw new Exception(Exception::USER_EMAIL_ALREADY_VERIFIED);
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
@ -2900,11 +3021,7 @@ App::post('/v1/account/verification/phone')
->inject('project')
->inject('locale')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale) {
$provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['sms'])
]));
if ($provider === false || $provider->isEmpty()) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@ -2912,6 +3029,10 @@ App::post('/v1/account/verification/phone')
throw new Exception(Exception::USER_PHONE_NOT_FOUND);
}
if ($user->getAttribute('phoneVerification')) {
throw new Exception(Exception::USER_PHONE_ALREADY_VERIFIED);
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
@ -2950,31 +3071,17 @@ App::post('/v1/account/verification/phone')
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$user->getAttribute('phone')]),
Query::equal('providerInternalId', [$provider->getInternalId()])
]);
if (!$target) {
$target = $dbForProject->createDocument('targets', new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerId' => $provider->getId(),
'providerInternalId' => $provider->getInternalId(),
'identifier' => $user->getAttribute('phone'),
]));
}
$messageDoc = $dbForProject->createDocument('messages', new Document([
$messageDoc = new Document([
'$id' => $verification->getId(),
'targets' => [$target->getId()],
'data' => [
'content' => $message,
],
]));
]);
$queueForMessaging
->setMessageId($messageDoc->getId())
->setMessage($messageDoc)
->setRecipients([$user->getAttribute('phone')])
->setProviderType('SMS')
->setProject($project)
->trigger();
@ -3053,3 +3160,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);
});

View file

@ -1571,7 +1571,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')

View file

@ -242,12 +242,16 @@ App::post('/v1/functions')
// Git connect logic
if (!empty($providerRepositoryId)) {
$teamId = $project->getAttribute('teamId', '');
$repository = $dbForConsole->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'installationId' => $installation->getId(),
'installationInternalId' => $installation->getInternalId(),

View file

@ -14,6 +14,7 @@ use Utopia\Registry\Registry;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
App::get('/v1/health')
@ -344,11 +345,20 @@ App::get('/v1/health/queue/webhooks')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queue')
->inject('response')
->action(function (Connection $queue, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$client = new Client(Event::WEBHOOK_QUEUE_NAME, $queue);
$response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/logs')
@ -362,11 +372,20 @@ App::get('/v1/health/queue/logs')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queue')
->inject('response')
->action(function (Connection $queue, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$client = new Client(Event::AUDITS_QUEUE_NAME, $queue);
$response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/certificates')
@ -380,11 +399,20 @@ App::get('/v1/health/queue/certificates')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queue')
->inject('response')
->action(function (Connection $queue, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$client = new Client(Event::CERTIFICATES_QUEUE_NAME, $queue);
$response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/builds')
@ -398,11 +426,20 @@ App::get('/v1/health/queue/builds')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queue')
->inject('response')
->action(function (Connection $queue, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$client = new Client(Event::BUILDS_QUEUE_NAME, $queue);
$response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/databases')
@ -417,11 +454,20 @@ App::get('/v1/health/queue/databases')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->param('name', 'database_db_main', new Text(256), 'Queue name for which to check the queue size', true)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queue')
->inject('response')
->action(function (string $name, Connection $queue, Response $response) {
->action(function (string $name, int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$client = new Client($name, $queue);
$response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/deletes')
@ -435,11 +481,20 @@ App::get('/v1/health/queue/deletes')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queue')
->inject('response')
->action(function (Connection $queue, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$client = new Client(Event::DELETE_QUEUE_NAME, $queue);
$response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/mails')
@ -453,11 +508,20 @@ App::get('/v1/health/queue/mails')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queue')
->inject('response')
->action(function (Connection $queue, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$client = new Client(Event::MAILS_QUEUE_NAME, $queue);
$response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/messaging')
@ -471,11 +535,20 @@ App::get('/v1/health/queue/messaging')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queue')
->inject('response')
->action(function (Connection $queue, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$client = new Client(Event::MESSAGING_QUEUE_NAME, $queue);
$response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/migrations')
@ -489,11 +562,20 @@ App::get('/v1/health/queue/migrations')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queue')
->inject('response')
->action(function (Connection $queue, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$client = new Client(Event::MIGRATIONS_QUEUE_NAME, $queue);
$response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/queue/functions')
@ -507,11 +589,20 @@ App::get('/v1/health/queue/functions')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_HEALTH_QUEUE)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queue')
->inject('response')
->action(function (Connection $queue, Response $response) {
->action(function (int|string $threshold, Connection $queue, Response $response) {
$threshold = \intval($threshold);
$client = new Client(Event::FUNCTIONS_QUEUE_NAME, $queue);
$response->dynamic(new Document([ 'size' => $client->getQueueSize() ]), Response::MODEL_HEALTH_QUEUE);
$size = $client->getQueueSize();
if ($size >= $threshold) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
}
$response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE);
}, ['response']);
App::get('/v1/health/storage/local')

View file

@ -1,5 +1,6 @@
<?php
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
@ -35,6 +36,8 @@ use Utopia\Validator\Text;
use MaxMind\Db\Reader;
use Utopia\Validator\WhiteList;
use function Swoole\Coroutine\batch;
App::post('/v1/messaging/providers/mailgun')
->desc('Create Mailgun provider')
->groups(['api', 'messaging'])
@ -68,7 +71,6 @@ App::post('/v1/messaging/providers/mailgun')
'provider' => 'mailgun',
'type' => 'email',
'enabled' => $enabled,
'search' => $providerId . ' ' . $name . ' ' . 'mailgun' . ' ' . 'email',
'credentials' => [
'apiKey' => $apiKey,
'domain' => $domain,
@ -79,16 +81,6 @@ App::post('/v1/messaging/providers/mailgun')
]
]);
// Check if a internal provider exists, if not, set this one as internal
if (
empty($dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['email'])
]))
) {
$provider->setAttribute('internal', true);
}
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
@ -119,7 +111,7 @@ App::post('/v1/messaging/providers/sendgrid')
->label('sdk.response.model', Response::MODEL_PROVIDER)
->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Provider name.')
->param('from', '', new Text(256), 'Sender email address.')
->param('from', '', new Email(), 'Sender email address.')
->param('apiKey', '', new Text(0), 'Sendgrid API key.')
->param('enabled', true, new Boolean(), 'Set as enabled.', true)
->inject('queueForEvents')
@ -133,7 +125,6 @@ App::post('/v1/messaging/providers/sendgrid')
'provider' => 'sendgrid',
'type' => 'email',
'enabled' => $enabled,
'search' => $providerId . ' ' . $name . ' ' . 'sendgrid' . ' ' . 'email',
'credentials' => [
'apiKey' => $apiKey,
],
@ -142,16 +133,6 @@ App::post('/v1/messaging/providers/sendgrid')
]
]);
// Check if a internal provider exists, if not, set this one as internal
if (
empty($dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['sms'])
]))
) {
$provider->setAttribute('internal', true);
}
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
@ -182,7 +163,7 @@ App::post('/v1/messaging/providers/msg91')
->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 Text(256), 'Sender number.')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->param('senderId', '', new Text(0), 'Msg91 Sender ID.')
->param('authKey', '', new Text(0), 'Msg91 Auth Key.')
->param('enabled', true, new Boolean(), 'Set as enabled.', true)
@ -196,7 +177,6 @@ App::post('/v1/messaging/providers/msg91')
'name' => $name,
'provider' => 'msg91',
'type' => 'sms',
'search' => $providerId . ' ' . $name . ' ' . 'msg91' . ' ' . 'sms',
'enabled' => $enabled,
'credentials' => [
'senderId' => $senderId,
@ -207,16 +187,6 @@ App::post('/v1/messaging/providers/msg91')
]
]);
// Check if a internal provider exists, if not, set this one as internal
if (
empty($dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['sms'])
]))
) {
$provider->setAttribute('internal', true);
}
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
@ -247,7 +217,7 @@ App::post('/v1/messaging/providers/telesign')
->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 Text(256), 'Sender number.')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->param('username', '', new Text(0), 'Telesign username.')
->param('password', '', new Text(0), 'Telesign password.')
->param('enabled', true, new Boolean(), 'Set as enabled.', true)
@ -261,7 +231,6 @@ App::post('/v1/messaging/providers/telesign')
'name' => $name,
'provider' => 'telesign',
'type' => 'sms',
'search' => $providerId . ' ' . $name . ' ' . 'telesign' . ' ' . 'sms',
'enabled' => $enabled,
'credentials' => [
'username' => $username,
@ -272,16 +241,6 @@ App::post('/v1/messaging/providers/telesign')
]
]);
// Check if a internal provider exists, if not, set this one as internal
if (
empty($dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['sms'])
]))
) {
$provider->setAttribute('internal', true);
}
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
@ -312,7 +271,7 @@ App::post('/v1/messaging/providers/textmagic')
->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 Text(256), 'Sender number.')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->param('username', '', new Text(0), 'Textmagic username.')
->param('apiKey', '', new Text(0), 'Textmagic apiKey.')
->param('enabled', true, new Boolean(), 'Set as enabled.', true)
@ -324,9 +283,8 @@ App::post('/v1/messaging/providers/textmagic')
$provider = new Document([
'$id' => $providerId,
'name' => $name,
'provider' => 'text-magic',
'provider' => 'textmagic',
'type' => 'sms',
'search' => $providerId . ' ' . $name . ' ' . 'text-magic' . ' ' . 'sms',
'enabled' => $enabled,
'credentials' => [
'username' => $username,
@ -337,16 +295,6 @@ App::post('/v1/messaging/providers/textmagic')
]
]);
// Check if a internal provider exists, if not, set this one as internal
if (
empty($dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['sms'])
]))
) {
$provider->setAttribute('internal', true);
}
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
@ -377,7 +325,7 @@ App::post('/v1/messaging/providers/twilio')
->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 Text(256), 'Sender number.')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->param('accountSid', '', new Text(0), 'Twilio account secret ID.')
->param('authToken', '', new Text(0), 'Twilio authentication token.')
->param('enabled', true, new Boolean(), 'Set as enabled.', true)
@ -391,7 +339,6 @@ App::post('/v1/messaging/providers/twilio')
'name' => $name,
'provider' => 'twilio',
'type' => 'sms',
'search' => $providerId . ' ' . $name . ' ' . 'twilio' . ' ' . 'sms',
'enabled' => $enabled,
'credentials' => [
'accountSid' => $accountSid,
@ -402,16 +349,6 @@ App::post('/v1/messaging/providers/twilio')
]
]);
// Check if a internal provider exists, if not, set this one as internal
if (
empty($dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['sms'])
]))
) {
$provider->setAttribute('internal', true);
}
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
@ -442,7 +379,7 @@ App::post('/v1/messaging/providers/vonage')
->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 Text(256), 'Sender number.')
->param('from', '', new Phone(), 'Sender Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->param('apiKey', '', new Text(0), 'Vonage API key.')
->param('apiSecret', '', new Text(0), 'Vonage API secret.')
->param('enabled', true, new Boolean(), 'Set as enabled.', true)
@ -456,7 +393,6 @@ App::post('/v1/messaging/providers/vonage')
'name' => $name,
'provider' => 'vonage',
'type' => 'sms',
'search' => $providerId . ' ' . $name . ' ' . 'vonage' . ' ' . 'sms',
'enabled' => $enabled,
'credentials' => [
'apiKey' => $apiKey,
@ -467,16 +403,6 @@ App::post('/v1/messaging/providers/vonage')
]
]);
// Check if a internal provider exists, if not, set this one as internal
if (
empty($dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['sms'])
]))
) {
$provider->setAttribute('internal', true);
}
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
@ -507,35 +433,24 @@ App::post('/v1/messaging/providers/fcm')
->label('sdk.response.model', Response::MODEL_PROVIDER)
->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('name', '', new Text(128), 'Provider name.')
->param('enabled', true, new Boolean(), 'Set as enabled.', true)
->param('serverKey', '', new Text(0), 'FCM server key.')
->param('enabled', true, new Boolean(), 'Set as enabled.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, bool $enabled, string $serverKey, Event $queueForEvents, Database $dbForProject, Response $response) {
->action(function (string $providerId, string $name, string $serverKey, bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) {
$providerId = $providerId == 'unique()' ? ID::unique() : $providerId;
$provider = new Document([
'$id' => $providerId,
'name' => $name,
'provider' => 'fcm',
'type' => 'push',
'search' => $providerId . ' ' . $name . ' ' . 'fcm' . ' ' . 'push',
'enabled' => $enabled,
'credentials' => [
'serverKey' => $serverKey,
],
]);
// Check if a internal provider exists, if not, set this one as internal
if (
empty($dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['push'])
]))
) {
$provider->setAttribute('internal', true);
}
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
@ -566,23 +481,22 @@ App::post('/v1/messaging/providers/apns')
->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('enabled', true, new Boolean(), 'Set as enabled.', true)
->param('authKey', '', new Text(0), 'APNS authentication key.')
->param('authKeyId', '', new Text(0), 'APNS authentication key ID.')
->param('teamId', '', new Text(0), 'APNS team ID.')
->param('bundleId', '', new Text(0), 'APNS bundle ID.')
->param('endpoint', '', new Text(0), 'APNS endpoint.')
->param('enabled', true, new Boolean(), 'Set as enabled.', 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) {
->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;
$provider = new Document([
'$id' => $providerId,
'name' => $name,
'provider' => 'apns',
'type' => 'push',
'search' => $providerId . ' ' . $name . ' ' . 'apns' . ' ' . 'push',
'enabled' => $enabled,
'credentials' => [
'authKey' => $authKey,
@ -593,16 +507,6 @@ App::post('/v1/messaging/providers/apns')
],
]);
// Check if a internal provider exists, if not, set this one as internal
if (
empty($dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['push'])
]))
) {
$provider->setAttribute('internal', true);
}
try {
$provider = $dbForProject->createDocument('providers', $provider);
} catch (DuplicateException) {
@ -784,15 +688,14 @@ App::patch('/v1/messaging/providers/mailgun/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true)
->param('isEuRegion', null, new Boolean(), 'Set as eu region.', true)
->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true)
->param('from', '', new Text(256), '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 $internal, ?bool $isEuRegion, string $from, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $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()) {
@ -806,7 +709,6 @@ App::patch('/v1/messaging/providers/mailgun/:providerId')
if (!empty($name)) {
$provider->setAttribute('name', $name);
$provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'mailgun' . ' ' . 'email');
}
if (!empty($from)) {
@ -819,10 +721,6 @@ App::patch('/v1/messaging/providers/mailgun/:providerId')
$provider->setAttribute('enabled', $enabled);
}
if ($internal === true) {
$provider->setAttribute('internal', $internal);
}
$credentials = $provider->getAttribute('credentials');
if ($isEuRegion === true || $isEuRegion === false) {
@ -841,15 +739,6 @@ App::patch('/v1/messaging/providers/mailgun/:providerId')
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
if ($internal === true) {
$internalProvider = $dbForProject->findOne('providers', [
'internal' => true,
'type' => 'email',
]);
$internalProvider->setAttribute('internal', false);
$dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider);
}
$queueForEvents
->setParam('providerId', $provider->getId());
@ -874,13 +763,12 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true)
->param('apiKey', '', new Text(0), 'Sendgrid API key.', true)
->param('from', '', new Text(256), 'Sender email address.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $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()) {
@ -894,7 +782,6 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
if (!empty($name)) {
$provider->setAttribute('name', $name);
$provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'sendgrid' . ' ' . 'email');
}
if (!empty($from)) {
@ -907,10 +794,6 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
$provider->setAttribute('enabled', $enabled);
}
if ($internal === true) {
$provider->setAttribute('internal', $internal);
}
if (!empty($apiKey)) {
$provider->setAttribute('credentials', [
'apiKey' => $apiKey,
@ -919,15 +802,6 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId')
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
if ($internal === true) {
$internalProvider = $dbForProject->findOne('providers', [
'internal' => true,
'type' => 'email',
]);
$internalProvider->setAttribute('internal', false);
$dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider);
}
$queueForEvents
->setParam('providerId', $provider->getId());
@ -952,14 +826,13 @@ App::patch('/v1/messaging/providers/msg91/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', 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, ?bool $internal, string $senderId, string $authKey, string $from, Event $queueForEvents, Database $dbForProject, Response $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()) {
@ -973,7 +846,6 @@ App::patch('/v1/messaging/providers/msg91/:providerId')
if (!empty($name)) {
$provider->setAttribute('name', $name);
$provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'msg91' . ' ' . 'sms');
}
if (!empty($from)) {
@ -986,10 +858,6 @@ App::patch('/v1/messaging/providers/msg91/:providerId')
$provider->setAttribute('enabled', $enabled);
}
if ($internal === true) {
$provider->setAttribute('internal', $internal);
}
$credentials = $provider->getAttribute('credentials');
if (!empty($senderId)) {
@ -1004,15 +872,6 @@ App::patch('/v1/messaging/providers/msg91/:providerId')
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
if ($internal === true) {
$internalProvider = $dbForProject->findOne('providers', [
'internal' => true,
'type' => 'email',
]);
$internalProvider->setAttribute('internal', false);
$dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider);
}
$queueForEvents
->setParam('providerId', $provider->getId());
@ -1037,14 +896,13 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', 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, ?bool $internal, string $username, string $password, string $from, Event $queueForEvents, Database $dbForProject, Response $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()) {
@ -1058,7 +916,6 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
if (!empty($name)) {
$provider->setAttribute('name', $name);
$provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'telesign' . ' ' . 'sms');
}
if (!empty($from)) {
@ -1071,10 +928,6 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
$provider->setAttribute('enabled', $enabled);
}
if ($internal === true) {
$provider->setAttribute('internal', $internal);
}
$credentials = $provider->getAttribute('credentials');
if (!empty($username)) {
@ -1089,15 +942,6 @@ App::patch('/v1/messaging/providers/telesign/:providerId')
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
if ($internal === true) {
$internalProvider = $dbForProject->findOne('providers', [
'internal' => true,
'type' => 'email',
]);
$internalProvider->setAttribute('internal', false);
$dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider);
}
$queueForEvents
->setParam('providerId', $provider->getId());
@ -1122,14 +966,13 @@ App::patch('/v1/messaging/providers/textmagic/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', 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, ?bool $internal, string $username, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $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()) {
@ -1137,13 +980,12 @@ App::patch('/v1/messaging/providers/textmagic/:providerId')
}
$providerAttr = $provider->getAttribute('provider');
if ($providerAttr !== 'text-magic') {
if ($providerAttr !== 'textmagic') {
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
}
if (!empty($name)) {
$provider->setAttribute('name', $name);
$provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'textmagic' . ' ' . 'sms');
}
if (!empty($from)) {
@ -1156,10 +998,6 @@ App::patch('/v1/messaging/providers/textmagic/:providerId')
$provider->setAttribute('enabled', $enabled);
}
if ($internal === true) {
$provider->setAttribute('internal', $internal);
}
$credentials = $provider->getAttribute('credentials');
if (!empty($username)) {
@ -1174,15 +1012,6 @@ App::patch('/v1/messaging/providers/textmagic/:providerId')
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
if ($internal === true) {
$internalProvider = $dbForProject->findOne('providers', [
'internal' => true,
'type' => 'email',
]);
$internalProvider->setAttribute('internal', false);
$dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider);
}
$queueForEvents
->setParam('providerId', $provider->getId());
@ -1207,14 +1036,13 @@ App::patch('/v1/messaging/providers/twilio/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true)
->param('accountSid', null, new Text(0), 'Twilio account secret ID.', true)
->param('authToken', null, 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, ?bool $internal, string $accountSid, string $authToken, string $from, Event $queueForEvents, Database $dbForProject, Response $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()) {
@ -1228,7 +1056,6 @@ App::patch('/v1/messaging/providers/twilio/:providerId')
if (!empty($name)) {
$provider->setAttribute('name', $name);
$provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'twilio' . ' ' . 'sms');
}
if (!empty($from)) {
@ -1241,10 +1068,6 @@ App::patch('/v1/messaging/providers/twilio/:providerId')
$provider->setAttribute('enabled', $enabled);
}
if ($internal === true) {
$provider->setAttribute('internal', $internal);
}
$credentials = $provider->getAttribute('credentials');
if (!empty($accountSid)) {
@ -1259,15 +1082,6 @@ App::patch('/v1/messaging/providers/twilio/:providerId')
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
if ($internal === true) {
$internalProvider = $dbForProject->findOne('providers', [
'internal' => true,
'type' => 'email',
]);
$internalProvider->setAttribute('internal', false);
$dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider);
}
$queueForEvents
->setParam('providerId', $provider->getId());
@ -1292,14 +1106,13 @@ App::patch('/v1/messaging/providers/vonage/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', 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, ?bool $internal, string $apiKey, string $apiSecret, string $from, Event $queueForEvents, Database $dbForProject, Response $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()) {
@ -1313,7 +1126,6 @@ App::patch('/v1/messaging/providers/vonage/:providerId')
if (!empty($name)) {
$provider->setAttribute('name', $name);
$provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'vonage' . ' ' . 'sms');
}
if (!empty($from)) {
@ -1326,10 +1138,6 @@ App::patch('/v1/messaging/providers/vonage/:providerId')
$provider->setAttribute('enabled', $enabled);
}
if ($internal === true) {
$provider->setAttribute('internal', $internal);
}
$credentials = $provider->getAttribute('credentials');
if (!empty($apiKey)) {
@ -1344,15 +1152,6 @@ App::patch('/v1/messaging/providers/vonage/:providerId')
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
if ($internal === true) {
$internalProvider = $dbForProject->findOne('providers', [
'internal' => true,
'type' => 'email',
]);
$internalProvider->setAttribute('internal', false);
$dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider);
}
$queueForEvents
->setParam('providerId', $provider->getId());
@ -1377,12 +1176,11 @@ App::patch('/v1/messaging/providers/fcm/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', true)
->param('serverKey', '', new Text(0), 'FCM Server Key.', true)
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $serverKey, Event $queueForEvents, Database $dbForProject, Response $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()) {
@ -1396,32 +1194,18 @@ App::patch('/v1/messaging/providers/fcm/:providerId')
if (!empty($name)) {
$provider->setAttribute('name', $name);
$provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'fcm' . ' ' . 'push');
}
if ($enabled === true || $enabled === false) {
$provider->setAttribute('enabled', $enabled);
}
if ($internal === true) {
$provider->setAttribute('internal', $internal);
}
if (!empty($serverKey)) {
$provider->setAttribute('credentials', ['serverKey' => $serverKey]);
}
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
if ($internal === true) {
$internalProvider = $dbForProject->findOne('providers', [
'internal' => true,
'type' => 'email',
]);
$internalProvider->setAttribute('internal', false);
$dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider);
}
$queueForEvents
->setParam('providerId', $provider->getId());
@ -1447,7 +1231,6 @@ App::patch('/v1/messaging/providers/apns/:providerId')
->param('providerId', '', new UID(), 'Provider ID.')
->param('name', '', new Text(128), 'Provider name.', true)
->param('enabled', null, new Boolean(), 'Set as enabled.', true)
->param('internal', null, new Boolean(), 'Set as internal. Internal providers are used in services other than Messaging service such as Authentication service', 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)
@ -1456,7 +1239,7 @@ App::patch('/v1/messaging/providers/apns/:providerId')
->inject('queueForEvents')
->inject('dbForProject')
->inject('response')
->action(function (string $providerId, string $name, ?bool $enabled, ?bool $internal, string $authKey, string $authKeyId, string $teamId, string $bundleId, string $endpoint, Event $queueForEvents, Database $dbForProject, Response $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()) {
@ -1470,17 +1253,12 @@ App::patch('/v1/messaging/providers/apns/:providerId')
if (!empty($name)) {
$provider->setAttribute('name', $name);
$provider->setAttribute('search', $provider->getId() . ' ' . $name . ' ' . 'apns' . ' ' . 'push');
}
if ($enabled === true || $enabled === false) {
$provider->setAttribute('enabled', $enabled);
}
if ($internal === true) {
$provider->setAttribute('internal', $internal);
}
$credentials = $provider->getAttribute('credentials');
if (!empty($authKey)) {
@ -1507,15 +1285,6 @@ App::patch('/v1/messaging/providers/apns/:providerId')
$provider = $dbForProject->updateDocument('providers', $provider->getId(), $provider);
if ($internal === true) {
$internalProvider = $dbForProject->findOne('providers', [
'internal' => true,
'type' => 'email',
]);
$internalProvider->setAttribute('internal', false);
$dbForProject->updateDocument('providers', $internalProvider->getId(), $internalProvider);
}
$queueForEvents
->setParam('providerId', $provider->getId());
@ -1588,9 +1357,6 @@ App::post('/v1/messaging/topics')
if ($description) {
$topic->setAttribute('description', $description);
$topic->setAttribute('search', $topic->getId() . ' ' . $name . ' ' . $description);
} else {
$topic->setAttribute('search', $topic->getId() . ' ' . $name);
}
try {
@ -1796,16 +1562,6 @@ App::patch('/v1/messaging/topics/:topicId')
$topic->setAttribute('description', $description);
}
if (!empty($name) || !empty($description)) {
if (!empty($name) && !empty($description)) {
$topic->setAttribute('search', $topic->getId() . ' ' . $name . ' ' . $description);
} elseif (!empty($name)) {
$topic->setAttribute('search', $topic->getId() . ' ' . $name . ' ' . $topic->getAttribute('description'));
} else {
$topic->setAttribute('search', $topic->getId() . ' ' . $topic->getAttribute('name') . ' ' . $description);
}
}
$topic = $dbForProject->updateDocument('topics', $topicId, $topic);
$queueForEvents
@ -1856,7 +1612,7 @@ App::delete('/v1/messaging/topics/:topicId')
});
App::post('/v1/messaging/topics/:topicId/subscribers')
->desc('Adds a subscriber to a topic.')
->desc('Create a subscriber.')
->groups(['api', 'messaging'])
->label('audits.event', 'subscriber.create')
->label('audits.resource', 'subscriber/{response.$id}')
@ -1869,9 +1625,9 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
->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 Topic ID or a new Topic ID.')
->param('topicId', '', new UID(), 'Topic ID.')
->param('targetId', '', new UID(), 'Target ID.')
->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')
@ -1890,12 +1646,16 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
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($target->getAttribute('userId'))),
Permission::read(Role::user($user->getId())),
Permission::delete(Role::user($target->getAttribute('userId'))),
],
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'topicId' => $topicId,
'topicInternalId' => $topic->getInternalId(),
'targetId' => $targetId,
@ -1913,13 +1673,15 @@ App::post('/v1/messaging/topics/:topicId/subscribers')
->setParam('topicId', $topic->getId())
->setParam('subscriberId', $subscriber->getId());
$subscriber->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 topic\'s subscribers.')
->desc('List subscribers.')
->groups(['api', 'messaging'])
->label('scope', 'subscribers.read')
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN, APP_AUTH_TYPE_KEY])
@ -1929,7 +1691,7 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
->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.')
->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)
->inject('dbForProject')
->inject('response')
@ -1959,9 +1721,20 @@ App::get('/v1/messaging/topics/:topicId/subscribers')
$cursor->setValue($cursorDocument);
}
$subscribers = $dbForProject->find('subscribers', $queries);
$subscribers = batch(\array_map(function (Document $subscriber) use ($dbForProject) {
return function () use ($subscriber, $dbForProject) {
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $subscriber->getAttribute('userId')));
return $subscriber
->setAttribute('userName', $user->getAttribute('name'));
};
}, $subscribers));
$response
->dynamic(new Document([
'subscribers' => $dbForProject->find('subscribers', $queries),
'subscribers' => $subscribers,
'total' => $dbForProject->count('subscribers', $queries, APP_LIMIT_COUNT),
]), Response::MODEL_SUBSCRIBER_LIST);
});
@ -2050,8 +1823,8 @@ App::get('/v1/messaging/subscribers/:subscriberId/logs')
]), Response::MODEL_LOG_LIST);
});
App::get('/v1/messaging/topics/:topicId/subscriber/:subscriberId')
->desc('Get a topic\'s subscriber.')
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])
@ -2061,7 +1834,7 @@ App::get('/v1/messaging/topics/:topicId/subscriber/:subscriberId')
->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.')
->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
->param('subscriberId', '', new UID(), 'Subscriber ID.')
->inject('dbForProject')
->inject('response')
@ -2078,12 +1851,16 @@ App::get('/v1/messaging/topics/:topicId/subscriber/:subscriberId')
throw new Exception(Exception::SUBSCRIBER_NOT_FOUND);
}
$user = Authorization::skip(fn () => $dbForProject->getDocument('users', $subscriber->getAttribute('userId')));
$subscriber->setAttribute('userName', $user->getAttribute('name'));
$response
->dynamic($subscriber, Response::MODEL_SUBSCRIBER);
});
App::delete('/v1/messaging/topics/:topicId/subscriber/:subscriberId')
->desc('Delete a subscriber from a topic.')
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}')
@ -2096,7 +1873,7 @@ App::delete('/v1/messaging/topics/:topicId/subscriber/:subscriberId')
->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.')
->param('topicId', '', new UID(), 'Topic ID. The topic ID subscribed to.')
->param('subscriberId', '', new UID(), 'Subscriber ID.')
->inject('queueForEvents')
->inject('dbForProject')
@ -2149,7 +1926,7 @@ App::post('/v1/messaging/messages/email')
->param('description', '', new Text(256), 'Description for message.', true)
->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true)
->param('html', false, new Boolean(), 'Is content of type HTML', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), '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')
@ -2174,7 +1951,6 @@ App::post('/v1/messaging/messages/email')
'html' => $html,
],
'status' => $status,
'search' => $messageId . ' ' . $description . ' ' . $subject,
]));
if ($status === 'processing') {
@ -2213,7 +1989,7 @@ App::post('/v1/messaging/messages/sms')
->param('targets', [], new ArrayList(new Text(Database::LENGTH_KEY), 1), 'List of Targets IDs.', true)
->param('description', '', new Text(256), 'Description for Message.', true)
->param('status', 'processing', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), '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')
@ -2236,7 +2012,6 @@ App::post('/v1/messaging/messages/sms')
'content' => $content,
],
'status' => $status,
'search' => $messageId . ' ' . $description,
]));
if ($status === 'processing') {
@ -2283,7 +2058,7 @@ App::post('/v1/messaging/messages/push')
->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', 'processing']), 'Message Status. Value must be either draft or processing.', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), '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')
@ -2338,7 +2113,6 @@ App::post('/v1/messaging/messages/push')
'deliveryTime' => $deliveryTime,
'data' => $pushData,
'status' => $status,
'search' => $messageId . ' ' . $description . ' ' . $title,
]));
if ($status === 'processing') {
@ -2530,7 +2304,7 @@ App::patch('/v1/messaging/messages/email/:messageId')
->param('content', '', new Text(64230), 'Email Content.', true)
->param('status', '', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true)
->param('html', false, new Boolean(), 'Is content of type HTML', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), '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')
@ -2583,8 +2357,6 @@ App::patch('/v1/messaging/messages/email/:messageId')
$message->setAttribute('description', $description);
}
$message->setAttribute('search', $message->getId() . ' ' . $message->getAttribute('description') . ' ' . $data['subject'] . ' ' . $message->getAttribute('providerId'));
if (!empty($status)) {
$message->setAttribute('status', $status);
}
@ -2630,7 +2402,7 @@ App::patch('/v1/messaging/messages/sms/:messageId')
->param('description', '', new Text(256), 'Description for Message.', true)
->param('content', '', new Text(64230), 'Email Content.', true)
->param('status', '', new WhiteList(['draft', 'processing']), 'Message Status. Value must be either draft or processing.', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), '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')
@ -2683,8 +2455,6 @@ App::patch('/v1/messaging/messages/sms/:messageId')
$message->setAttribute('deliveryTime', $deliveryTime);
}
$message->setAttribute('search', $message->getId() . ' ' . $message->getAttribute('description') . ' ' . $message->getAttribute('providerId'));
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === 'processing') {
@ -2729,7 +2499,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
->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', 'processing']), 'Message Status. Value must be either draft or processing.', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), 'Delivery time for message in ISO 8601 format. DateTime value must be in future.', true)
->param('deliveryTime', null, new DatetimeValidator(requireDateInFuture: true), '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')
@ -2814,8 +2584,6 @@ App::patch('/v1/messaging/messages/push/:messageId')
$message->setAttribute('deliveryTime', $deliveryTime);
}
$message->setAttribute('search', $message->getId() . ' ' . $message->getAttribute('description') . ' ' . $pushData['title'] . ' ' . $message->getAttribute('providerId'));
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === 'processing') {

View file

@ -1154,7 +1154,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) {
@ -1271,7 +1271,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) {

View file

@ -628,12 +628,7 @@ App::post('/v1/teams/:teamId/memberships')
->trigger()
;
} elseif (!empty($phone)) {
$provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [
Query::equal('internal', [true]),
Query::equal('type', ['sms'])
]));
if ($provider === false || $provider->isEmpty()) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@ -647,28 +642,17 @@ App::post('/v1/teams/:teamId/memberships')
$message = $message->setParam('{{token}}', $url);
$message = $message->render();
$target = $dbForProject->createDocument('targets', new Document([
'userId' => $invitee->getId(),
'userInternalId' => $invitee->getInternalId(),
'providerId' => $provider->getId(),
'providerInternalId' => $provider->getInternalId(),
'identifier' => $phone,
]));
$messageDoc = $dbForProject->createDocument('messages', new Document([
// Here membership ID is used as message ID so that it can be used in test cases to verify the message
'$id' => $membership->getId(),
'targets' => [$target->getId()],
$messageDoc = new Document([
'$id' => ID::unique(),
'data' => [
'content' => $message,
],
'providerId' => $provider->getId(),
'providerInternalId' => $provider->getInternalId(),
'deliveryTime' => Datetime::now(),
]));
]);
$queueForMessaging
->setMessageId($messageDoc->getId())
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType('SMS')
->setProject($project)
->trigger();
}

View file

@ -394,18 +394,25 @@ App::post('/v1/users/:userId/targets')
->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('targetId', '', new UID(), 'Target ID.')
->param('providerId', '', new UID(), 'Provider ID.')
->param('providerType', '', new WhiteList(['email', 'sms', '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)
->inject('queueForEvents')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, string $providerId, string $identifier, Event $queueForEvents, Response $response, Database $dbForProject) {
$provider = $dbForProject->getDocument('providers', $providerId);
->action(function (string $targetId, string $userId, string $providerType, string $identifier, string $providerId, Event $queueForEvents, Response $response, Database $dbForProject) {
$targetId = $targetId == 'unique()' ? ID::unique() : $targetId;
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
$provider = new Document();
if ($providerType === 'push') {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
}
$user = $dbForProject->getDocument('users', $userId);
@ -423,8 +430,9 @@ App::post('/v1/users/:userId/targets')
try {
$target = $dbForProject->createDocument('targets', new Document([
'$id' => $targetId,
'providerId' => $providerId,
'providerInternalId' => $provider->getInternalId(),
'providerId' => $providerId ?? null,
'providerInternalId' => $provider->getInternalId() ?? null,
'providerType' => $providerType,
'userId' => $userId,
'userInternalId' => $user->getInternalId(),
'identifier' => $identifier,
@ -1223,8 +1231,8 @@ App::patch('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::patch('/v1/users/:userId/targets/:targetId/identifier')
->desc('Update user target\'s identifier')
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}')
@ -1232,19 +1240,19 @@ App::patch('/v1/users/:userId/targets/:targetId/identifier')
->label('scope', 'targets.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateTargetIdentifier')
->label('sdk.description', '/docs/references/users/update-target-identifier.md')
->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.)')
->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('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)', true)
->inject('queueForEvents')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, string $identifier, Event $queueForEvents, Response $response, Database $dbForProject) {
->action(function (string $userId, string $targetId, string $providerId, string $identifier, Event $queueForEvents, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
@ -1261,7 +1269,20 @@ App::patch('/v1/users/:userId/targets/:targetId/identifier')
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$target->setAttribute('identifier', $identifier);
if ($identifier) {
$target->setAttribute('identifier', $identifier);
}
if ($providerId) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
$target->setAttribute('providerId', $provider->getId());
$target->setAttribute('providerInternalId', $provider->getInternalId());
}
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
$dbForProject->deleteCachedDocument('users', $user->getId());

View file

@ -857,10 +857,10 @@ App::post('/v1/vcs/github/events')
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
//find functionId from functions table
$repositories = $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::limit(100),
]);
]));
// create new deployment only on push and not when branch is created
if (!$providerBranchCreated) {
@ -877,13 +877,13 @@ App::post('/v1/vcs/github/events')
]);
foreach ($installations as $installation) {
$repositories = $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
Query::equal('installationInternalId', [$installation->getInternalId()]),
Query::limit(1000)
]);
]));
foreach ($repositories as $repository) {
$dbForConsole->deleteDocument('repositories', $repository->getId());
Authorization::skip(fn () => $dbForConsole->deleteDocument('repositories', $repository->getId()));
}
$dbForConsole->deleteDocument('installations', $installation->getId());
@ -915,10 +915,10 @@ App::post('/v1/vcs/github/events')
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitMessage = $commitDetails["commitMessage"] ?? '';
$repositories = $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]);
]));
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForConsole, $queueForBuilds, $getProjectDB, $request);
} elseif ($parsedPayload["action"] == "closed") {
@ -929,10 +929,10 @@ App::post('/v1/vcs/github/events')
$external = $parsedPayload["external"] ?? true;
if ($external) {
$repositories = $dbForConsole->find('repositories', [
$repositories = Authorization::skip(fn () => $dbForConsole->find('repositories', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::orderDesc('$createdAt')
]);
]));
foreach ($repositories as $repository) {
$providerPullRequestIds = $repository->getAttribute('providerPullRequestIds', []);
@ -1046,8 +1046,8 @@ App::delete('/v1/vcs/installations/:installationId')
->inject('response')
->inject('project')
->inject('dbForConsole')
->inject('deletes')
->action(function (string $installationId, Response $response, Document $project, Database $dbForConsole, Delete $deletes) {
->inject('queueForDeletes')
->action(function (string $installationId, Response $response, Document $project, Database $dbForConsole, Delete $queueForDeletes) {
$installation = $dbForConsole->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
@ -1058,7 +1058,7 @@ App::delete('/v1/vcs/installations/:installationId')
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove installation from DB');
}
$deletes
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($installation);
@ -1092,9 +1092,9 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$repository = $dbForConsole->getDocument('repositories', $repositoryId, [
$repository = Authorization::skip(fn () => $dbForConsole->getDocument('repositories', $repositoryId, [
Query::equal('projectInternalId', [$project->getInternalId()])
]);
]));
if ($repository->isEmpty()) {
throw new Exception(Exception::REPOSITORY_NOT_FOUND);
@ -1109,7 +1109,7 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
// TODO: Delete from array when PR is closed
$repository = $dbForConsole->updateDocument('repositories', $repository->getId(), $repository);
$repository = Authorization::skip(fn () => $dbForConsole->updateDocument('repositories', $repository->getId(), $repository));
$privateKey = App::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = App::getEnv('_APP_VCS_GITHUB_APP_ID');

View file

@ -105,8 +105,8 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return
const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours
const APP_USER_ACCCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 516;
const APP_VERSION_STABLE = '1.4.11';
const APP_CACHE_BUSTER = 327;
const APP_VERSION_STABLE = '1.4.12';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@ -557,6 +557,72 @@ Database::addFilter(
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);
if (\array_key_exists('subject', $data)) {
$searchValues = \array_merge($searchValues, [$data['subject'], 'email']);
} elseif (\array_key_exists('content', $data)) {
$searchValues = \array_merge($searchValues, [$data['content'], 'sms']);
} else {
$searchValues = \array_merge($searchValues, [$data['title'], 'push']);
}
$search = \implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
/**
* DB Formats
*/

View file

@ -0,0 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php patch-recreate-repositories-documents $@

14
composer.lock generated
View file

@ -1906,16 +1906,16 @@
},
{
"name": "utopia-php/database",
"version": "0.45.1",
"version": "0.45.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "0e76f996439b80794ab73c2fffdb51ebd6676e4b"
"reference": "dc789f2c1fd8b5ee07ff883e11c9ad7970824788"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/0e76f996439b80794ab73c2fffdb51ebd6676e4b",
"reference": "0e76f996439b80794ab73c2fffdb51ebd6676e4b",
"url": "https://api.github.com/repos/utopia-php/database/zipball/dc789f2c1fd8b5ee07ff883e11c9ad7970824788",
"reference": "dc789f2c1fd8b5ee07ff883e11c9ad7970824788",
"shasum": ""
},
"require": {
@ -1956,9 +1956,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.45.1"
"source": "https://github.com/utopia-php/database/tree/0.45.2"
},
"time": "2023-11-01T08:30:19+00:00"
"time": "2023-11-15T03:38:47+00:00"
},
{
"name": "utopia-php/domains",
@ -5822,5 +5822,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.3.0"
}

View file

@ -587,6 +587,8 @@ services:
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_SMS_FROM
- _APP_SMS_PROVIDER
appwrite-worker-migrations:
entrypoint: worker-migrations

View file

@ -9,7 +9,11 @@ use Utopia\Queue\Client;
class Messaging extends Event
{
protected ?string $messageId = null;
private ?string $deliveryTime = null;
protected ?Document $message = null;
protected ?array $recipients = null;
protected ?string $deliveryTime = null;
protected ?string $providerType = null;
public function __construct(protected Connection $connection)
{
@ -20,6 +24,52 @@ class Messaging extends Event
->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.
*
@ -43,6 +93,29 @@ class Messaging extends Event
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 Delivery time for the messaging event.
*
@ -92,6 +165,9 @@ class Messaging extends Event
'project' => $this->project,
'user' => $this->user,
'messageId' => $this->messageId,
'message' => $this->message,
'recipients' => $this->recipients,
'providerType' => $this->providerType,
]);
}
}

View file

@ -84,8 +84,11 @@ class Exception extends \Exception
public const USER_OAUTH2_BAD_REQUEST = 'user_oauth2_bad_request';
public const USER_OAUTH2_UNAUTHORIZED = 'user_oauth2_unauthorized';
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';
public const TEAM_INVITE_ALREADY_EXISTS = 'team_invite_already_exists';
@ -236,6 +239,7 @@ class Exception extends \Exception
public const PROVIDER_NOT_FOUND = 'provider_not_found';
public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists';
public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type';
public const PROVIDER_INTERNAL_UPDATE_DISABLED = 'provider_internal_update_disabled';
/** Topic */
public const TOPIC_NOT_FOUND = 'topic_not_found';

View file

@ -76,6 +76,7 @@ abstract class Migration
'1.4.9' => 'V19',
'1.4.10' => 'V19',
'1.4.11' => 'V19',
'1.4.12' => 'V19'
];
/**

View file

@ -19,6 +19,7 @@ use Appwrite\Platform\Tasks\VolumeSync;
use Appwrite\Platform\Tasks\CalcTierStats;
use Appwrite\Platform\Tasks\Upgrade;
use Appwrite\Platform\Tasks\DeleteOrphanedProjects;
use Appwrite\Platform\Tasks\PatchRecreateRepositoriesDocuments;
class Tasks extends Service
{
@ -42,6 +43,7 @@ class Tasks extends Service
->addAction(Specs::getName(), new Specs())
->addAction(CalcTierStats::getName(), new CalcTierStats())
->addAction(DeleteOrphanedProjects::getName(), new DeleteOrphanedProjects())
->addAction(PatchRecreateRepositoriesDocuments::getName(), new PatchRecreateRepositoriesDocuments())
;
}

View file

@ -2,17 +2,16 @@
namespace Appwrite\Platform\Tasks;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Pools\Group;
use Utopia\Registry\Registry;
use Utopia\Validator\Boolean;
class DeleteOrphanedProjects extends Action
{
@ -25,18 +24,19 @@ class DeleteOrphanedProjects extends Action
{
$this
->desc('Get stats for projects')
->desc('Delete orphaned projects')
->param('commit', false, new Boolean(true), 'Commit project deletion', true)
->inject('pools')
->inject('cache')
->inject('dbForConsole')
->inject('register')
->callback(function (Group $pools, Cache $cache, Database $dbForConsole, Registry $register) {
$this->action($pools, $cache, $dbForConsole, $register);
->callback(function (bool $commit, Group $pools, Cache $cache, Database $dbForConsole, Registry $register) {
$this->action($commit, $pools, $cache, $dbForConsole, $register);
});
}
public function action(Group $pools, Cache $cache, Database $dbForConsole, Registry $register): void
public function action(bool $commit, Group $pools, Cache $cache, Database $dbForConsole, Registry $register): void
{
Console::title('Delete orphaned projects V1');
@ -55,6 +55,7 @@ class DeleteOrphanedProjects extends Action
Console::success("Found a total of: {$totalProjects} projects");
$orphans = 0;
$cnt = 0;
$count = 0;
$limit = 30;
$sum = 30;
@ -79,19 +80,43 @@ class DeleteOrphanedProjects extends Action
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDefaultDatabase('appwrite');
$dbForProject->setNamespace('_' . $project->getInternalId());
$collectionsCreated = $dbForProject->count(Database::METADATA);
$message = ' (' . $collectionsCreated . ') collections where found on project (' . $project->getId() . '))';
if ($collectionsCreated < (count($collectionsConfig) + 2)) {
Console::error($message);
$orphans++;
} else {
Console::log($message);
$collectionsCreated = 0;
$cnt++;
if ($dbForProject->exists($dbForProject->getDefaultDatabase(), Database::METADATA)) {
$collectionsCreated = $dbForProject->count(Database::METADATA);
}
} catch (\Throwable $th) {
//$dbForConsole->deleteDocument('projects', $project->getId());
//Console::success('Deleting project (' . $project->getId() . ')');
Console::error(' (0) collections where found for project (' . $project->getId() . ')');
$msg = '(' . $cnt . ') found (' . $collectionsCreated . ') collections on project (' . $project->getInternalId() . ') , database (' . $project['database'] . ')';
/**
* +2 = audit+abuse
*/
if ($collectionsCreated >= (count($collectionsConfig) + 2)) {
Console::log($msg . ' ignoring....');
continue;
}
Console::log($msg);
if ($collectionsCreated > 0) {
$collections = $dbForProject->find(Database::METADATA, []);
foreach ($collections as $collection) {
if ($commit) {
$dbForProject->deleteCollection($collection->getId());
$dbForConsole->deleteCachedCollection($collection->getId());
}
Console::info('--Deleting collection (' . $collection->getId() . ') project no (' . $project->getInternalId() . ')');
}
}
if ($commit) {
$dbForConsole->deleteDocument('projects', $project->getId());
$dbForConsole->deleteCachedDocument('projects', $project->getId());
}
Console::info('--Deleting project no (' . $project->getInternalId() . ')');
$orphans++;
} catch (\Throwable $th) {
Console::error('Error: ' . $th->getMessage());
} finally {
$pools
->get($db)
@ -110,6 +135,6 @@ class DeleteOrphanedProjects extends Action
$count = $count + $sum;
}
Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects found ' . $orphans . ' orphans');
Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects found ' . $orphans . ' orphans');
}
}

View file

@ -0,0 +1,169 @@
<?php
namespace Appwrite\Platform\Tasks;
use Utopia\Platform\Action;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Validator\Text;
class PatchRecreateRepositoriesDocuments extends Action
{
public static function getName(): string
{
return 'patch-recreate-repositories-documents';
}
public function __construct()
{
$this
->desc('Recreate missing repositories in consoleDB from projectDBs. They can be missing if you used Appwrite 1.4.10 or 1.4.11, and deleted a function.')
->param('after', '', new Text(36), 'After cursor', true)
->param('projectId', '', new Text(36), 'Select project to validate', true)
->inject('dbForConsole')
->inject('getProjectDB')
->callback(fn ($after, $projectId, $dbForConsole, $getProjectDB) => $this->action($after, $projectId, $dbForConsole, $getProjectDB));
}
public function action($after, $projectId, Database $dbForConsole, callable $getProjectDB): void
{
Console::info("Starting the patch");
$startTime = microtime(true);
if (!empty($projectId)) {
try {
$project = $dbForConsole->getDocument('projects', $projectId);
$dbForProject = call_user_func($getProjectDB, $project);
$this->recreateRepositories($dbForConsole, $dbForProject, $project);
} catch (\Throwable $th) {
Console::error("Unexpected error occured with Project ID {$projectId}");
Console::error('[Error] Type: ' . get_class($th));
Console::error('[Error] Message: ' . $th->getMessage());
Console::error('[Error] File: ' . $th->getFile());
Console::error('[Error] Line: ' . $th->getLine());
}
} else {
$queries = [];
if (!empty($after)) {
Console::info("Iterating remaining projects after project with ID {$after}");
$project = $dbForConsole->getDocument('projects', $after);
$queries = [Query::cursorAfter($project)];
} else {
Console::info("Iterating all projects");
}
$this->foreachDocument($dbForConsole, 'projects', $queries, function (Document $project) use ($getProjectDB, $dbForConsole) {
$projectId = $project->getId();
try {
$dbForProject = call_user_func($getProjectDB, $project);
$this->recreateRepositories($dbForConsole, $dbForProject, $project);
} catch (\Throwable $th) {
Console::error("Unexpected error occured with Project ID {$projectId}");
Console::error('[Error] Type: ' . get_class($th));
Console::error('[Error] Message: ' . $th->getMessage());
Console::error('[Error] File: ' . $th->getFile());
Console::error('[Error] Line: ' . $th->getLine());
}
});
}
$endTime = microtime(true);
$timeTaken = $endTime - $startTime;
$hours = (int)($timeTaken / 3600);
$timeTaken -= $hours * 3600;
$minutes = (int)($timeTaken / 60);
$timeTaken -= $minutes * 60;
$seconds = (int)$timeTaken;
$milliseconds = ($timeTaken - $seconds) * 1000;
Console::info("Recreate patch completed in $hours h, $minutes m, $seconds s, $milliseconds mis ( total $timeTaken milliseconds)");
}
protected function foreachDocument(Database $database, string $collection, array $queries = [], callable $callback = null): void
{
$limit = 1000;
$results = [];
$sum = $limit;
$latestDocument = null;
while ($sum === $limit) {
$newQueries = $queries;
if ($latestDocument != null) {
array_unshift($newQueries, Query::cursorAfter($latestDocument));
}
$newQueries[] = Query::limit($limit);
$results = $database->find($collection, $newQueries);
if (empty($results)) {
return;
}
$sum = count($results);
foreach ($results as $document) {
if (is_callable($callback)) {
$callback($document);
}
}
$latestDocument = $results[array_key_last($results)];
}
}
public function recreateRepositories(Database $dbForConsole, Database $dbForProject, Document $project): void
{
$projectId = $project->getId();
Console::log("Running patch for project {$projectId}");
$this->foreachDocument($dbForProject, 'functions', [], function (Document $function) use ($dbForProject, $dbForConsole, $project) {
$isConnected = !empty($function->getAttribute('providerRepositoryId', ''));
if ($isConnected) {
$repository = $dbForConsole->getDocument('repositories', $function->getAttribute('repositoryId', ''));
if ($repository->isEmpty()) {
$projectId = $project->getId();
$functionId = $function->getId();
Console::success("Recreating repositories document for project ID {$projectId}, function ID {$functionId}");
$repository = $dbForConsole->createDocument('repositories', new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'installationId' => $function->getAttribute('installationId', ''),
'installationInternalId' => $function->getAttribute('installationInternalId', ''),
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'providerRepositoryId' => $function->getAttribute('providerRepositoryId', ''),
'resourceId' => $function->getId(),
'resourceInternalId' => $function->getInternalId(),
'resourceType' => 'function',
'providerPullRequestIds' => []
]));
$function = $dbForProject->updateDocument('functions', $function->getId(), $function
->setAttribute('repositoryId', $repository->getId())
->setAttribute('repositoryInternalId', $repository->getInternalId()));
$this->foreachDocument($dbForProject, 'deployments', [
Query::equal('resourceInternalId', [$function->getInternalId()]),
Query::equal('resourceType', ['functions'])
], function (Document $deployment) use ($dbForProject, $repository) {
$dbForProject->updateDocument('deployments', $deployment->getId(), $deployment
->setAttribute('repositoryId', $repository->getId())
->setAttribute('repositoryInternalId', $repository->getInternalId()));
});
}
}
});
}
}

View file

@ -753,14 +753,15 @@ class Deletes extends Action
*/
Console::info("Deleting VCS repositories and comments linked to function " . $functionId);
$this->deleteByGroup('repositories', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::equal('resourceInternalId', [$functionInternalId]),
Query::equal('resourceType', ['function']),
], $dbForConsole, function (Document $document) use ($dbForConsole) {
$providerRepositoryId = $document->getAttribute('providerRepositoryId', '');
$projectId = $document->getAttribute('projectId', '');
$projectInternalId = $document->getAttribute('projectInternalId', '');
$this->deleteByGroup('vcsComments', [
Query::equal('providerRepositoryId', [$providerRepositoryId]),
Query::equal('projectId', [$projectId]),
Query::equal('projectInternalId', [$projectInternalId]),
], $dbForConsole);
});

View file

@ -3,14 +3,16 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Extend\Exception;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Helpers\ID;
use Utopia\DSN\DSN;
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\Database\Validator\Authorization;
use Utopia\Messaging\Adapters\SMS as SMSAdapter;
use Utopia\Messaging\Adapters\SMS\Mock;
use Utopia\Messaging\Adapters\SMS\Msg91;
@ -64,9 +66,15 @@ class Messaging extends Action
return;
}
$message = $dbForProject->getDocument('messages', $payload['messageId']);
if (!\is_null($payload['message']) && !\is_null($payload['recipients'])) {
if ($payload['providerType'] === 'SMS') {
$this->processInternalSMSMessage(new Document($payload['message']), $payload['recipients']);
}
} else {
$message = $dbForProject->getDocument('messages', $payload['messageId']);
$this->processMessage($dbForProject, $message);
$this->processMessage($dbForProject, $message);
}
}
private function processMessage(Database $dbForProject, Document $message): void
@ -99,28 +107,56 @@ class Messaging extends Action
$recipients = \array_merge($recipients, $targets);
}
$internalProvider = $dbForProject->findOne('providers', [
Query::equal('enabled', [true]),
Query::equal('type', [$recipients[0]->getAttribute('providerType')]),
]);
/**
* @var array<string, array<string>> $identifiersByProviderId
*/
$identifiersByProviderId = [];
/**
* @var Document[] $providers
*/
$providers = [];
foreach ($recipients as $recipient) {
$providerId = $recipient->getAttribute('providerId');
if (!isset($providers[$providerId])) {
$providers[$providerId] = [];
if (!$providerId) {
$providerId = $internalProvider->getId();
}
$providers[$providerId][] = $recipient->getAttribute('identifier');
if (!isset($identifiersByProviderId[$providerId])) {
$identifiersByProviderId[$providerId] = [];
}
$identifiersByProviderId[$providerId][] = $recipient->getAttribute('identifier');
}
/**
* @var array[] $results
*/
$results = batch(\array_map(function ($providerId) use ($providers, $message, $dbForProject) {
return function () use ($providerId, $providers, $message, $dbForProject) {
$provider = $dbForProject->getDocument('providers', $providerId);
$identifiers = $providers[$providerId];
$results = batch(\array_map(function ($providerId) use ($identifiersByProviderId, $providers, $internalProvider, $message, $dbForProject) {
return function () use ($providerId, $identifiersByProviderId, $providers, $internalProvider, $message, $dbForProject) {
$provider = new Document();
if ($internalProvider->getId() === $providerId) {
$provider = $internalProvider;
} else {
$provider = $dbForProject->getDocument('providers', $providerId);
}
$providers[] = $provider;
$identifiers = $identifiersByProviderId[$providerId];
$adapter = match ($provider->getAttribute('type')) {
'sms' => $this->sms($provider),
'push' => $this->push($provider),
'email' => $this->email($provider),
default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
};
$maxBatchSize = $adapter->getMaxMessagesPerRequest();
$batches = \array_chunk($identifiers, $maxBatchSize);
$batchIndex = 0;
@ -131,12 +167,14 @@ class Messaging extends Action
$deliveryErrors = [];
$messageData = clone $message;
$messageData->setAttribute('to', $batch);
$data = match ($provider->getAttribute('type')) {
'sms' => $this->buildSMSMessage($messageData, $provider),
'push' => $this->buildPushMessage($messageData),
'email' => $this->buildEmailMessage($messageData, $provider),
default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
};
try {
$adapter->send($data);
$deliveredTotal += \count($batch);
@ -154,16 +192,18 @@ class Messaging extends Action
return $results;
};
}, \array_keys($providers)));
}, \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) {
@ -171,13 +211,88 @@ class Messaging extends Action
} 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;
}
$smsDSN = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$host = $smsDSN->getHost();
$password = $smsDSN->getPassword();
$user = $smsDSN->getUser();
$from = App::getEnv('_APP_SMS_FROM');
$provider = new Document([
'$id' => ID::unique(),
'provider' => $host,
'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
]
]);
$adapter = $this->sms($provider);
$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
{
}
@ -188,7 +303,7 @@ class Messaging extends Action
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']),
'text-magic' => new TextMagic($credentials['username'], $credentials['apiKey']),
'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']),

View file

@ -8,6 +8,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use InfluxDB\Database as InfluxDatabase;
use DateTime;
use Utopia\Database\Validator\Authorization;
use Utopia\Registry\Registry;
class TimeSeries extends Calculator
@ -426,32 +427,34 @@ class TimeSeries extends Calculator
$project = $this->database->getDocument('projects', $projectId);
$database = call_user_func($this->getProjectDB, $project);
try {
$document = $database->getDocument('stats', $id);
if ($document->isEmpty()) {
$database->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
'metric' => $metric,
'value' => $value,
'type' => $type,
'region' => $this->region,
]));
} else {
$database->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
Authorization::skip(function () use ($database, $id, $period, $time, $metric, $value, $type, $projectId) {
try {
$document = $database->getDocument('stats', $id);
if ($document->isEmpty()) {
$database->createDocument('stats', new Document([
'$id' => $id,
'period' => $period,
'time' => $time,
'metric' => $metric,
'value' => $value,
'type' => $type,
'region' => $this->region,
]));
} else {
$database->updateDocument(
'stats',
$document->getId(),
$document->setAttribute('value', $value)
);
}
} catch (\Exception $e) { // if projects are deleted this might fail
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}");
} else {
throw $e;
}
}
} catch (\Exception $e) { // if projects are deleted this might fail
if (is_callable($this->errorHandler)) {
call_user_func($this->errorHandler, $e, "sync_project_{$projectId}_metric_{$metric}");
} else {
throw $e;
}
}
});
$this->register->get('pools')->reclaim();
}

View file

@ -8,7 +8,6 @@ class Providers extends Base
'name',
'provider',
'type',
'internal',
'enabled',
];

View file

@ -40,12 +40,6 @@ class Provider extends Model
'default' => '',
'example' => 'mailgun',
])
->addRule('internal', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is this a pre-configured provider instance?',
'default' => false,
'example' => true,
])
->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Is provider enabled?',

View file

@ -34,6 +34,18 @@ class Subscriber extends Model
'default' => '',
'example' => '259125845563242502',
])
->addRule('userId', [
'type' => self::TYPE_STRING,
'description' => 'User 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.',

View file

@ -41,6 +41,12 @@ class Target extends Model
'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' => 'email',
])
->addRule('identifier', [
'type' => self::TYPE_STRING,
'description' => 'The target identifier.',

View file

@ -945,6 +945,32 @@ trait AccountBase
return $data;
}
/**
* @depends testUpdateAccountVerification
*/
public function testCreateAccountVerificationForVerifiedEmail($data): array
{
$email = $data['email'] ?? '';
$name = $data['name'] ?? '';
$session = $data['session'] ?? '';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/account/verification', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'url' => 'http://localhost/verification',
]);
$this->assertEquals(409, $response['headers']['status-code']);
return $data;
}
/**
* @depends testUpdateAccountVerification
*/

View file

@ -744,33 +744,8 @@ class AccountCustomClientTest extends Scope
public function testCreatePhone(): array
{
if (empty(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'))) {
$this->markTestSkipped('SMS DSN not provided');
}
$number = '+123456789';
$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($authKey) || empty($senderId)) {
$this->markTestSkipped('SMS provider not configured');
}
$number = $to;
$response = $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' => 'Sms provider',
'senderId' => $senderId,
'authKey' => $authKey,
'from' => $from,
]);
$this->assertEquals(201, $response['headers']['status-code']);
/**
* Test for SUCCESS
*/
@ -781,7 +756,6 @@ class AccountCustomClientTest extends Scope
]), [
'userId' => ID::unique(),
'phone' => $number,
'from' => $from,
]);
$this->assertEquals(201, $response['headers']['status-code']);
@ -807,19 +781,17 @@ class AccountCustomClientTest extends Scope
\sleep(5);
$message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $messageId, [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$smsRequest = $this->getLastRequest();
$this->assertEquals(200, $message['headers']['status-code']);
$this->assertEquals(1, $message['body']['deliveredTotal']);
$this->assertEquals(0, \count($message['body']['deliveryErrors']));
$this->assertEquals('http://request-catcher:5000/mock-sms', $smsRequest['url']);
$this->assertEquals('Appwrite Mock Message Sender', $smsRequest['headers']['User-Agent']);
$this->assertEquals('username', $smsRequest['headers']['X-Username']);
$this->assertEquals('password', $smsRequest['headers']['X-Key']);
$this->assertEquals('POST', $smsRequest['method']);
$this->assertEquals('+123456789', $smsRequest['data']['from']);
$this->assertEquals($number, $smsRequest['data']['to']);
$data['token'] = $message['body']['data']['content'];
$data['token'] = $smsRequest['data']['message'];
$data['id'] = $userId;
$data['number'] = $number;
@ -1018,8 +990,6 @@ class AccountCustomClientTest extends Scope
public function testPhoneVerification(array $data): array
{
$session = $data['session'] ?? '';
$smsDSN = new DSN(App::getEnv('_APP_MESSAGE_SMS_TEST_DSN'));
$from = $smsDSN->getParam('from');
/**
* Test for SUCCESS
@ -1030,28 +1000,19 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), ['from' => $from]);
]));
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEmpty($response['body']['secret']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
\sleep(3);
\sleep(2);
$message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $response['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']));
$smsRequest = $this->getLastRequest();
return \array_merge($data, [
'token' => $message['body']['data']['content']
'token' => $smsRequest['data']['secret']
]);
}
@ -1108,4 +1069,27 @@ class AccountCustomClientTest extends Scope
return $data;
}
/**
* @depends testPhoneVerification
*/
#[Retry(count: 1)]
public function testPhoneVerificationForVerifiedPhone(array $data): array
{
$session = $data['session'] ?? '';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/account/verification/phone', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals(409, $response['headers']['status-code']);
return $data;
}
}

View file

@ -124,38 +124,7 @@ class AccountTest extends Scope
*/
public function testCreatePhoneVerification(): array
{
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($authKey) || empty($senderId)) {
$this->markTestSkipped('SMS provider not configured');
}
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_MSG91_PROVIDER);
$graphQLPayload = [
'query' => $query,
'variables' => [
'providerId' => ID::unique(),
'name' => 'Sms Provider',
'from' => $from,
'senderId' => $senderId,
'authKey' => $authKey,
],
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $graphQLPayload);
$query = $this->getQuery(self::$CREATE_PHONE_VERIFICATION);
$graphQLPayload = [

View file

@ -934,10 +934,11 @@ trait Base
}
}';
case self::$CREATE_USER_TARGET:
return 'mutation createUserTarget($userId: String!, $targetId: String!, $providerId: String!, $identifier: String!){
usersCreateTarget(userId: $userId, targetId: $targetId, providerId: $providerId, identifier: $identifier) {
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
}
@ -949,6 +950,7 @@ trait Base
targets {
_id
userId
providerType
providerId
identifier
}
@ -959,15 +961,17 @@ trait Base
usersGetTarget(userId: $userId, targetId: $targetId) {
_id
userId
providerType
providerId
identifier
}
}';
case self::$UPDATE_USER_TARGET:
return 'mutation updateUserTarget($userId: String!, $targetId: String!, $identifier: String!){
usersUpdateTargetIdentifier(userId: $userId, targetId: $targetId, identifier: $identifier) {
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
}
@ -1792,7 +1796,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1803,7 +1806,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1814,7 +1816,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1825,7 +1826,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1836,7 +1836,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1847,7 +1846,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1858,7 +1856,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1869,7 +1866,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1880,7 +1876,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1893,7 +1888,7 @@ trait Base
name
provider
type
internal
enabled
}
}
@ -1905,7 +1900,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1916,7 +1910,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1927,7 +1920,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1938,7 +1930,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1949,7 +1940,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1960,7 +1950,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1971,7 +1960,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1982,7 +1970,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -1993,7 +1980,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -2004,7 +1990,6 @@ trait Base
name
provider
type
internal
enabled
}
}';
@ -2059,6 +2044,7 @@ trait Base
return 'mutation createSubscriber($subscriberId: String!, $targetId: String!, $topicId: String!) {
messagingCreateSubscriber(subscriberId: $subscriberId, targetId: $targetId, topicId: $topicId) {
_id
userId
targetId
topicId
}
@ -2069,6 +2055,7 @@ trait Base
total
subscribers {
_id
userId
targetId
topicId
}
@ -2078,6 +2065,7 @@ trait Base
return 'query getSubscriber($topicId: String!, $subscriberId: String!) {
messagingGetSubscriber(topicId: $topicId, subscriberId: $subscriberId) {
_id
userId
targetId
topicId
}

View file

@ -395,6 +395,7 @@ class MessagingTest extends Scope
'query' => $query,
'variables' => [
'targetId' => ID::unique(),
'providerType' => 'email',
'userId' => $userId,
'providerId' => $providerId,
'identifier' => 'token',
@ -427,20 +428,23 @@ class MessagingTest extends Scope
], $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']['userId'], $userId);
return $response['body']['data']['messagingCreateSubscriber'];
}
/**
* @depends testUpdateTopic
* @depends testCreateSubscriber
*/
public function testListSubscribers(string $topicId)
public function testListSubscribers(array $subscriber)
{
$query = $this->getQuery(self::$LIST_SUBSCRIBERS);
$graphQLPayload = [
'query' => $query,
'variables' => [
'topicId' => $topicId,
'topicId' => $subscriber['topicId'],
],
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
@ -450,6 +454,9 @@ class MessagingTest extends Scope
]), $graphQLPayload);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($response['body']['data']['messagingListSubscribers']['subscribers'][0]['topicId'], $subscriber['topicId']);
$this->assertEquals($response['body']['data']['messagingListSubscribers']['subscribers'][0]['targetId'], $subscriber['targetId']);
$this->assertEquals($response['body']['data']['messagingListSubscribers']['subscribers'][0]['userId'], $subscriber['userId']);
$this->assertEquals(1, \count($response['body']['data']['messagingListSubscribers']['subscribers']));
}
@ -478,6 +485,9 @@ class MessagingTest extends Scope
$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['userId'], $response['body']['data']['messagingGetSubscriber']['userId']);
}
/**
@ -604,6 +614,7 @@ class MessagingTest extends Scope
'query' => $query,
'variables' => [
'targetId' => ID::unique(),
'providerType' => 'email',
'userId' => $user['body']['data']['usersCreate']['_id'],
'providerId' => $providerId,
'identifier' => $to,
@ -678,119 +689,13 @@ class MessagingTest extends Scope
*/
public function testUpdateEmail(array $email)
{
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' => 'Mailgun2',
'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' => 'random2-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(),
'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(),
'status' => 'draft',
'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']],
'topics' => [$email['topics'][0]],
'subject' => 'Khali beats Undertaker',
'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
],
@ -916,6 +821,7 @@ class MessagingTest extends Scope
'query' => $query,
'variables' => [
'targetId' => ID::unique(),
'providerType' => 'sms',
'userId' => $user['body']['data']['usersCreate']['_id'],
'providerId' => $providerId,
'identifier' => $to,
@ -988,117 +894,13 @@ class MessagingTest extends Scope
*/
public function testUpdateSMS(array $sms)
{
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-2',
'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' => 'random4-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(),
'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(),
'status' => 'draft',
'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']],
'topics' => [$sms['topics'][0]],
'content' => '345463',
],
];
@ -1219,6 +1021,7 @@ class MessagingTest extends Scope
'query' => $query,
'variables' => [
'targetId' => ID::unique(),
'providerType' => 'push',
'userId' => $user['body']['data']['usersCreate']['_id'],
'providerId' => $providerId,
'identifier' => $to,
@ -1293,112 +1096,13 @@ class MessagingTest extends Scope
*/
public function testUpdatePushNotification(array $push)
{
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' => 'FCM2',
'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-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(),
'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(),
'status' => 'draft',
'topics' => [$topic['body']['data']['messagingCreateTopic']['_id']],
'topics' => [$push['topics'][0]],
'title' => 'Push Notification Title',
'body' => 'Push Notifiaction Body',
],

View file

@ -78,6 +78,7 @@ class UsersTest extends Scope
'variables' => [
'targetId' => ID::unique(),
'userId' => $user['_id'],
'providerType' => 'email',
'providerId' => $providerId,
'identifier' => 'identifier',
]
@ -479,7 +480,7 @@ class UsersTest extends Scope
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $target['headers']['status-code']);
$this->assertEquals('newidentifier', $target['body']['data']['usersUpdateTargetIdentifier']['identifier']);
$this->assertEquals('newidentifier', $target['body']['data']['usersUpdateTarget']['identifier']);
}
public function testDeleteUserSessions()

View file

@ -138,6 +138,15 @@ class HealthCustomServerTest extends Scope
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/webhooks?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(500, $response['headers']['status-code']);
return [];
}
@ -155,6 +164,15 @@ class HealthCustomServerTest extends Scope
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/logs?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(500, $response['headers']['status-code']);
return [];
}
@ -172,6 +190,15 @@ class HealthCustomServerTest extends Scope
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/certificates?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(500, $response['headers']['status-code']);
return [];
}
@ -189,6 +216,15 @@ class HealthCustomServerTest extends Scope
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/functions?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(500, $response['headers']['status-code']);
return [];
}
@ -206,6 +242,15 @@ class HealthCustomServerTest extends Scope
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/builds?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(500, $response['headers']['status-code']);
return [];
}
@ -225,6 +270,18 @@ class HealthCustomServerTest extends Scope
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'database_db_main',
'threshold' => '0'
]);
$this->assertEquals(500, $response['headers']['status-code']);
return [];
}
@ -242,6 +299,15 @@ class HealthCustomServerTest extends Scope
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/deletes?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(500, $response['headers']['status-code']);
return [];
}
@ -259,6 +325,15 @@ class HealthCustomServerTest extends Scope
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/mails?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(500, $response['headers']['status-code']);
return [];
}
@ -276,6 +351,15 @@ class HealthCustomServerTest extends Scope
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/messaging?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(500, $response['headers']['status-code']);
return [];
}
@ -293,6 +377,15 @@ class HealthCustomServerTest extends Scope
$this->assertIsInt($response['body']['size']);
$this->assertLessThan(100, $response['body']['size']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/health/queue/migrations?threshold=0', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(500, $response['headers']['status-code']);
return [];
}

View file

@ -317,9 +317,11 @@ trait MessagingBase
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'targetId' => ID::unique(),
'providerType' => 'email',
'providerId' => $provider['body']['$id'],
'identifier' => 'my-token',
]);
$this->assertEquals(201, $target['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topic['$id'] . '/subscribers', \array_merge([
@ -329,13 +331,16 @@ trait MessagingBase
'subscriberId' => ID::unique(),
'targetId' => $target['body']['$id'],
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals($target['body']['userId'], $response['body']['userId']);
$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']);
@ -344,6 +349,7 @@ trait MessagingBase
return [
'topicId' => $topic['body']['$id'],
'targetId' => $target['body']['$id'],
'userId' => $target['body']['userId'],
'subscriberId' => $response['body']['$id']
];
}
@ -353,14 +359,16 @@ trait MessagingBase
*/
public function testGetSubscriber(array $data)
{
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $data['topicId'] . '/subscriber/' . $data['subscriberId'], \array_merge([
$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']['userId']);
}
/**
@ -376,6 +384,7 @@ trait MessagingBase
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
$this->assertEquals($data['userId'], $response['body']['subscribers'][0]['userId']);
$this->assertEquals(\count($response['body']['subscribers']), $response['body']['total']);
return $data;
@ -496,7 +505,7 @@ trait MessagingBase
*/
public function testDeleteSubscriber(array $data)
{
$response = $this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $data['topicId'] . '/subscriber/' . $data['subscriberId'], \array_merge([
$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()));
@ -558,6 +567,7 @@ trait MessagingBase
'isEuRegion' => filter_var($isEuRegion, FILTER_VALIDATE_BOOLEAN),
'from' => $from
]);
$this->assertEquals(201, $provider['headers']['status-code']);
// Create Topic
@ -570,6 +580,7 @@ trait MessagingBase
'name' => 'topic1',
'description' => 'Test Topic'
]);
$this->assertEquals(201, $topic['headers']['status-code']);
// Create User
@ -593,6 +604,7 @@ trait MessagingBase
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'targetId' => ID::unique(),
'providerType' => 'email',
'providerId' => $provider['body']['$id'],
'identifier' => $to,
]);
@ -645,21 +657,6 @@ trait MessagingBase
*/
public function testUpdateEmail(array $email)
{
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');
}
$message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -669,71 +666,6 @@ trait MessagingBase
// Test failure as the message has already been sent.
$this->assertEquals(400, $message['headers']['status-code']);
// 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-2',
'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' => 'random-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(),
'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',
@ -742,7 +674,7 @@ trait MessagingBase
], [
'messageId' => ID::unique(),
'status' => 'draft',
'topics' => [$topic['body']['$id']],
'topics' => [$email['body']['topics'][0]],
'subject' => 'Khali beats Undertaker',
'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
]);
@ -801,6 +733,7 @@ trait MessagingBase
'authKey' => $authKey,
'from' => $from
]);
$this->assertEquals(201, $provider['headers']['status-code']);
// Create Topic
@ -813,6 +746,7 @@ trait MessagingBase
'name' => 'topic1',
'description' => 'Test Topic'
]);
$this->assertEquals(201, $topic['headers']['status-code']);
// Create User
@ -836,6 +770,7 @@ trait MessagingBase
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'targetId' => ID::unique(),
'providerType' => 'sms',
'providerId' => $provider['body']['$id'],
'identifier' => $to,
]);
@ -887,20 +822,6 @@ trait MessagingBase
*/
public function testUpdateSMS(array $sms)
{
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');
}
$message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/sms/' . $sms['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -910,70 +831,6 @@ trait MessagingBase
// Test failure as the message has already been sent.
$this->assertEquals(400, $message['headers']['status-code']);
// 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-2',
'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' => 'random2-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(),
'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',
@ -982,7 +839,7 @@ trait MessagingBase
], [
'messageId' => ID::unique(),
'status' => 'draft',
'topics' => [$topic['body']['$id']],
'topics' => [$sms['body']['topics'][0]],
'content' => '047487',
]);
@ -1036,6 +893,7 @@ trait MessagingBase
'name' => 'FCM-1',
'serverKey' => $serverKey,
]);
$this->assertEquals(201, $provider['headers']['status-code']);
// Create Topic
@ -1048,6 +906,7 @@ trait MessagingBase
'name' => 'topic1',
'description' => 'Test Topic'
]);
$this->assertEquals(201, $topic['headers']['status-code']);
// Create User
@ -1071,6 +930,7 @@ trait MessagingBase
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'targetId' => ID::unique(),
'providerType' => 'push',
'providerId' => $provider['body']['$id'],
'identifier' => $to,
]);
@ -1123,18 +983,6 @@ trait MessagingBase
*/
public function testUpdatePushNotification(array $push)
{
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');
}
$message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/push/' . $push['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -1144,68 +992,6 @@ trait MessagingBase
// Test failure as the message has already been sent.
$this->assertEquals(400, $message['headers']['status-code']);
// 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-2',
'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' => 'random4-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(),
'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',
@ -1214,7 +1000,7 @@ trait MessagingBase
], [
'messageId' => ID::unique(),
'status' => 'draft',
'topics' => [$topic['body']['$id']],
'topics' => [$push['body']['topics'][0]],
'title' => 'Test-Notification',
'body' => 'Test-Notification-Body',
]);

View file

@ -1232,7 +1232,7 @@ trait UsersBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'providerId' => 'unique()',
'providerId' => ID::unique(),
'name' => 'Sengrid1',
'apiKey' => 'my-apikey',
'from' => 'from@domain.com',
@ -1244,6 +1244,7 @@ trait UsersBase
], $this->getHeaders()), [
'targetId' => ID::unique(),
'providerId' => $provider['body']['$id'],
'providerType' => 'email',
'identifier' => 'my-token',
]);
$this->assertEquals(201, $response['headers']['status-code']);
@ -1257,7 +1258,7 @@ trait UsersBase
*/
public function testUpdateUserTarget(array $data): array
{
$response = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/targets/' . $data['$id'] . '/identifier', array_merge([
$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()), [
@ -1303,11 +1304,14 @@ trait UsersBase
'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(0, $response['body']['total']);
}

View file

@ -636,6 +636,120 @@ class WebhooksCustomClientTest extends Scope
return $data;
}
/**
* @depends testUpdateAccountPrefs
*/
public function testCreateAccountVerification($data): array
{
$id = $data['id'] ?? '';
$email = $data['email'] ?? '';
$session = $data['session'] ?? '';
$verification = $this->client->call(Client::METHOD_POST, '/account/verification', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'url' => 'http://localhost/verification',
]);
$verificationId = $verification['body']['$id'];
$this->assertEquals(201, $verification['headers']['status-code']);
$this->assertIsArray($verification['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.verification.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.verification.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.*.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.*.verification.{$verificationId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertNotEmpty($webhook['data']['userId']);
$this->assertNotEmpty($webhook['data']['secret']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['expire']));
$data['secret'] = $webhook['data']['secret'];
return $data;
}
/**
* @depends testCreateAccountVerification
*/
public function testUpdateAccountVerification($data): array
{
$id = $data['id'] ?? '';
$email = $data['email'] ?? '';
$session = $data['session'] ?? '';
$secret = $data['secret'] ?? '';
$verification = $this->client->call(Client::METHOD_PUT, '/account/verification', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'userId' => $id,
'secret' => $secret,
]);
$verificationId = $verification['body']['$id'];
$this->assertEquals(200, $verification['headers']['status-code']);
$this->assertIsArray($verification['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.verification.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.verification.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.*.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.*.verification.{$verificationId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertNotEmpty($webhook['data']['userId']);
$this->assertNotEmpty($webhook['data']['secret']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['expire']));
$data['secret'] = $webhook['data']['secret'];
return $data;
}
/**
* @depends testUpdateAccountPrefs
*/
@ -751,120 +865,6 @@ class WebhooksCustomClientTest extends Scope
return $data;
}
/**
* @depends testUpdateAccountPrefs
*/
public function testCreateAccountVerification($data): array
{
$id = $data['id'] ?? '';
$email = $data['email'] ?? '';
$session = $data['session'] ?? '';
$verification = $this->client->call(Client::METHOD_POST, '/account/verification', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'url' => 'http://localhost/verification',
]);
$verificationId = $verification['body']['$id'];
$this->assertEquals(201, $verification['headers']['status-code']);
$this->assertIsArray($verification['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.verification.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.verification.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.*.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.*.verification.{$verificationId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertNotEmpty($webhook['data']['userId']);
$this->assertNotEmpty($webhook['data']['secret']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['expire']));
$data['secret'] = $webhook['data']['secret'];
return $data;
}
/**
* @depends testCreateAccountVerification
*/
public function testUpdateAccountVerification($data): array
{
$id = $data['id'] ?? '';
$email = $data['email'] ?? '';
$session = $data['session'] ?? '';
$secret = $data['secret'] ?? '';
$verification = $this->client->call(Client::METHOD_PUT, '/account/verification', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'userId' => $id,
'secret' => $secret,
]);
$verificationId = $verification['body']['$id'];
$this->assertEquals(200, $verification['headers']['status-code']);
$this->assertIsArray($verification['body']);
$webhook = $this->getLastRequest();
$signatureKey = $this->getProject()['signatureKey'];
$payload = json_encode($webhook['data']);
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.verification.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.verification.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.*.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.*.verification.{$verificationId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}.verification.{$verificationId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertNotEmpty($webhook['data']['userId']);
$this->assertNotEmpty($webhook['data']['secret']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['expire']));
$data['secret'] = $webhook['data']['secret'];
return $data;
}
/**
* @depends testCreateTeamMembership
*/